mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 15:29:43 -04:00
Compare commits
53 Commits
anims
...
727d9c6c22
| Author | SHA1 | Date | |
|---|---|---|---|
| 727d9c6c22 | |||
| 908e1f600e | |||
| 270d800df2 | |||
| d445d182ea | |||
| 476256c9e7 | |||
| 06ea7373f7 | |||
| 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 }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: core/go.mod
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v6
|
||||
# with:
|
||||
# token: ${{ steps.app_token.outputs.token }}
|
||||
# fetch-depth: 0
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
TAG: ${{ inputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
git checkout ${TAG}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Download core artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: core-assets-*
|
||||
merge-multiple: true
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
rpm -qpi "$SRPM"
|
||||
|
||||
- name: Upload SRPM artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||
path: ${{ steps.build.outputs.srpm_path }}
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -195,10 +195,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Wait before OBS upload
|
||||
run: sleep 3
|
||||
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
@@ -344,7 +347,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -157,12 +157,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
cache: false
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
+3
-1
@@ -86,7 +86,9 @@ touch .qmlls.ini
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||
ASSETS_DIR=assets
|
||||
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
|
||||
|
||||
@@ -32,6 +32,9 @@ clean:
|
||||
@$(MAKE) -C $(CORE_DIR) clean
|
||||
@echo "Clean complete"
|
||||
|
||||
lint-qml:
|
||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||
|
||||
# Installation targets
|
||||
install-bin:
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@@ -130,6 +133,7 @@ help:
|
||||
@echo " all (default) - Build the DMS binary"
|
||||
@echo " build - Same as 'all'"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||
@echo ""
|
||||
@echo "Install:"
|
||||
@echo " install - Build and install everything (requires sudo)"
|
||||
|
||||
@@ -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) {
|
||||
var data []byte
|
||||
copyFromStdin := false
|
||||
|
||||
switch {
|
||||
case len(args) > 0:
|
||||
data = []byte(args[0])
|
||||
default:
|
||||
case clipCopyDownload || clipCopyType == "__multi__":
|
||||
var err error
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatalf("read stdin: %v", err)
|
||||
}
|
||||
default:
|
||||
copyFromStdin = true
|
||||
}
|
||||
|
||||
if clipCopyDownload {
|
||||
@@ -257,6 +260,13 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
if copyFromStdin {
|
||||
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||
log.Fatalf("copy: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||
log.Fatalf("copy: %v", err)
|
||||
}
|
||||
|
||||
+1111
-157
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,10 @@ import (
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
PersistentPreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
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().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
// Add subcommands to update
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
@@ -11,29 +11,20 @@ import (
|
||||
var Version = "dev"
|
||||
|
||||
func init() {
|
||||
// Add flags
|
||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Block root
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
@@ -7,14 +7,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func findCommandPath(cmd string) (string, error) {
|
||||
path, err := exec.LookPath(cmd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func isArchPackageInstalled(packageName string) bool {
|
||||
cmd := exec.Command("pacman", "-Q", packageName)
|
||||
err := cmd.Run()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
@@ -12,17 +14,37 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
if foreground {
|
||||
return copyServeWithWriter(func(writer io.Writer) error {
|
||||
total := 0
|
||||
for total < len(data) {
|
||||
n, err := writer.Write(data[total:])
|
||||
total += n
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if total != len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}, mimeType, pasteOnce)
|
||||
}
|
||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
return 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"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
@@ -30,11 +52,15 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if stdinSource, ok := data.(*os.File); ok {
|
||||
cmd.Stdin = stdinSource
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
@@ -44,16 +70,66 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
if _, err := stdin.Write(data); err != nil {
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
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("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
@@ -139,12 +215,18 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
file.Write(data)
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -165,6 +247,8 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
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, ", ")))
|
||||
|
||||
hasNiri := false
|
||||
hasQuickshell := false
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri-git" {
|
||||
hasNiri = true
|
||||
}
|
||||
if pkg == "quickshell" || pkg == "quickshell-git" {
|
||||
hasQuickshell = true
|
||||
}
|
||||
}
|
||||
|
||||
// If quickshell is in the list, always reinstall google-breakpad first
|
||||
if hasQuickshell {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.63,
|
||||
Step: "Reinstalling google-breakpad for quickshell...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
||||
}
|
||||
|
||||
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
||||
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||
// since we manually manage those dependencies
|
||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||
// Pre-install dependencies from .SRCINFO
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
if pkg == "dms-shell-bin" {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||
}
|
||||
} else {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
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",
|
||||
}
|
||||
|
||||
// Install dependencies and makedepends explicitly
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
// Install dependencies from .SRCINFO
|
||||
depFilter := ""
|
||||
if pkg == "dms-shell-git" {
|
||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
||||
}
|
||||
|
||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`
|
||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||
if [[ "%s" == *"quickshell"* ]]; then
|
||||
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
||||
fi
|
||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||
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 {
|
||||
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 {
|
||||
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{
|
||||
@@ -677,7 +656,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||
buildCmd.Dir = packageDir
|
||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // 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 {
|
||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||
|
||||
@@ -92,9 +92,25 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
return debianPackageInstalledPrecisely(pkg)
|
||||
}
|
||||
|
||||
func debianPackageInstalledPrecisely(pkg string) bool {
|
||||
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(output)) == "installed"
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
@@ -194,12 +210,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
@@ -379,6 +395,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
||||
return names
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||
if minimal {
|
||||
args = append(args, "--no-install-recommends")
|
||||
}
|
||||
return append(args, packages...)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
@@ -482,20 +506,46 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
||||
|
||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
startProgress := 0.40
|
||||
endProgress := 0.60
|
||||
if totalGroups > 1 {
|
||||
if groupIndex == 1 {
|
||||
endProgress = 0.50
|
||||
} else {
|
||||
startProgress = 0.50
|
||||
}
|
||||
}
|
||||
|
||||
args := d.aptInstallArgs(groupPackages, minimal)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: startProgress,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
|
||||
@@ -484,28 +484,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
||||
|
||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri" || pkg == "niri-git" {
|
||||
args = append(args, "--setopt=install_weak_deps=False")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
@@ -515,26 +494,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
||||
|
||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri" || pkg == "niri-git" {
|
||||
args = append(args, "--setopt=install_weak_deps=False")
|
||||
break
|
||||
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
if minimal {
|
||||
args = append(args, "--setopt=install_weak_deps=False")
|
||||
}
|
||||
return append(args, packages...)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
groupStart := startProgress
|
||||
groupEnd := endProgress
|
||||
if totalGroups > 1 {
|
||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||
if groupIndex == 1 {
|
||||
groupEnd = midpoint
|
||||
} else {
|
||||
groupStart = midpoint
|
||||
}
|
||||
}
|
||||
|
||||
args := f.dnfInstallArgs(groupPackages, minimal)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: groupStart,
|
||||
Step: step,
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing COPR packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package distros
|
||||
|
||||
type minimalInstallGroup struct {
|
||||
packages []string
|
||||
minimal bool
|
||||
}
|
||||
|
||||
func shouldPreferMinimalInstall(pkg string) bool {
|
||||
switch pkg {
|
||||
case "niri", "niri-git":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
||||
for _, pkg := range packages {
|
||||
if shouldPreferMinimalInstall(pkg) {
|
||||
minimal = append(minimal, pkg)
|
||||
continue
|
||||
}
|
||||
normal = append(normal, pkg)
|
||||
}
|
||||
return normal, minimal
|
||||
}
|
||||
|
||||
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
||||
normal, minimal := splitMinimalInstallPackages(packages)
|
||||
groups := make([]minimalInstallGroup, 0, 2)
|
||||
if len(minimal) > 0 {
|
||||
groups = append(groups, minimalInstallGroup{
|
||||
packages: minimal,
|
||||
minimal: true,
|
||||
})
|
||||
}
|
||||
if len(normal) > 0 {
|
||||
groups = append(groups, minimalInstallGroup{
|
||||
packages: normal,
|
||||
minimal: false,
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
@@ -29,6 +29,8 @@ type OpenSUSEDistribution struct {
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
||||
|
||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &OpenSUSEDistribution{
|
||||
@@ -199,35 +201,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"make",
|
||||
"unzip",
|
||||
"gcc",
|
||||
"gcc-c++",
|
||||
"cmake",
|
||||
"ninja",
|
||||
"pkgconf-pkg-config",
|
||||
"git",
|
||||
"qt6-base-devel",
|
||||
"qt6-declarative-devel",
|
||||
"qt6-declarative-private-devel",
|
||||
"qt6-shadertools",
|
||||
"qt6-shadertools-devel",
|
||||
"qt6-wayland-devel",
|
||||
"qt6-waylandclient-private-devel",
|
||||
"spirv-tools-devel",
|
||||
"cli11-devel",
|
||||
"wayland-protocols-devel",
|
||||
"libgbm-devel",
|
||||
"libdrm-devel",
|
||||
"pipewire-devel",
|
||||
"jemalloc-devel",
|
||||
"wayland-utils",
|
||||
"Mesa-libGLESv3-devel",
|
||||
"pam-devel",
|
||||
"glib2-devel",
|
||||
"polkit-devel",
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
@@ -297,6 +271,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
||||
}
|
||||
|
||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
@@ -327,7 +305,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -342,7 +320,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||
}
|
||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -432,9 +410,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
||||
}
|
||||
}
|
||||
|
||||
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
||||
|
||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||
}
|
||||
|
||||
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
||||
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{openSUSENiriWaylandServerPackage}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||
for _, pkg := range extraPkgs {
|
||||
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||
continue
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
||||
systemPkgs = append(systemPkgs, pkg)
|
||||
}
|
||||
|
||||
return systemPkgs
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
@@ -514,27 +515,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
func isOpenSUSEInstallMediaURI(uri string) bool {
|
||||
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
||||
|
||||
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
||||
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
||||
strings.HasPrefix(normalizedURI, "hd:/") ||
|
||||
strings.HasPrefix(normalizedURI, "iso:/")
|
||||
}
|
||||
|
||||
func parseZypperInstallMediaAliases(output string) []string {
|
||||
var aliases []string
|
||||
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || !strings.Contains(line, "|") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, "|")
|
||||
if len(parts) < 7 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
|
||||
alias := parts[1]
|
||||
enabled := strings.ToLower(parts[3])
|
||||
uri := parts[len(parts)-1]
|
||||
|
||||
if alias == "" || strings.EqualFold(alias, "alias") {
|
||||
continue
|
||||
}
|
||||
if enabled != "" && enabled != "yes" {
|
||||
continue
|
||||
}
|
||||
if !isOpenSUSEInstallMediaURI(uri) {
|
||||
continue
|
||||
}
|
||||
|
||||
aliases = append(aliases, alias)
|
||||
}
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
||||
output, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
||||
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
||||
}
|
||||
|
||||
aliases := parseZypperInstallMediaAliases(string(output))
|
||||
if len(aliases) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.055,
|
||||
Step: "Disabling install media repositories...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
||||
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
||||
}
|
||||
|
||||
for _, alias := range aliases {
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
||||
repoOutput, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
||||
}
|
||||
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
if minimal {
|
||||
args = append(args, "--no-recommends")
|
||||
}
|
||||
return append(args, packages...)
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
groupStart := startProgress
|
||||
groupEnd := endProgress
|
||||
if totalGroups > 1 {
|
||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||
if groupIndex == 1 {
|
||||
groupEnd = midpoint
|
||||
} else {
|
||||
groupStart = midpoint
|
||||
}
|
||||
}
|
||||
|
||||
args := o.zypperInstallArgs(groupPackages, minimal)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: groupStart,
|
||||
Step: step,
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
|
||||
@@ -100,9 +100,7 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
return debianPackageInstalledPrecisely(pkg)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
@@ -454,21 +452,7 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
@@ -477,21 +461,59 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
||||
}
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||
if minimal {
|
||||
args = append(args, "--no-install-recommends")
|
||||
}
|
||||
return append(args, packages...)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing PPA packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
groupStart := startProgress
|
||||
groupEnd := endProgress
|
||||
if totalGroups > 1 {
|
||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||
if groupIndex == 1 {
|
||||
groupEnd = midpoint
|
||||
} else {
|
||||
groupStart = midpoint
|
||||
}
|
||||
}
|
||||
|
||||
args := u.aptInstallArgs(groupPackages, minimal)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: groupStart,
|
||||
Step: step,
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||
}
|
||||
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
+1143
-112
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: "vscode", Kind: TemplateKindVSCode},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||
{ID: "zed", Commands: []string{"zed"}, ConfigFile: "zed.toml"},
|
||||
}
|
||||
|
||||
func (c *ColorMode) GTKTheme() string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
|
||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||
result, err := DetectNetworkStack()
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||
t.Skipf("system D-Bus unavailable: %v", err)
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
if assert.NotNil(t, result) {
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ Architecture: any
|
||||
Depends: ${misc:Depends},
|
||||
greetd,
|
||||
quickshell-git | quickshell
|
||||
Recommends: niri | hyprland | sway
|
||||
Suggests: niri | hyprland | sway
|
||||
Description: DankMaterialShell greeter for greetd
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
|
||||
@@ -102,6 +102,19 @@ if [[ ! -d "distro/debian" ]]; then
|
||||
echo "Error: Run this script from the repository root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
|
||||
osc_retry() {
|
||||
local max=3 attempt=1
|
||||
while true; do
|
||||
if osc "$@"; then return 0; fi
|
||||
((attempt >= max)) && return 1
|
||||
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
|
||||
sleep $((5*attempt))
|
||||
((attempt++))
|
||||
done
|
||||
}
|
||||
|
||||
# Parameters:
|
||||
# $1 = PROJECT
|
||||
# $2 = PACKAGE
|
||||
@@ -309,8 +322,23 @@ mkdir -p "$OBS_BASE"
|
||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||
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"
|
||||
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
||||
echo "Error: Checkout failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
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)
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1145,7 +1173,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
||||
else
|
||||
echo "==> Committing to OBS"
|
||||
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]}
|
||||
set -e
|
||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||
|
||||
@@ -13,7 +13,7 @@ Architecture: any
|
||||
Depends: ${misc:Depends},
|
||||
greetd,
|
||||
quickshell-git | quickshell
|
||||
Recommends: niri | hyprland | sway
|
||||
Suggests: niri | hyprland | sway
|
||||
Description: DankMaterialShell greeter for greetd
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
|
||||
@@ -72,76 +72,82 @@
|
||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||
in
|
||||
{
|
||||
dms-shell = pkgs.buildGoModule (
|
||||
let
|
||||
rootSrc = ./.;
|
||||
in
|
||||
dms-shell = pkgs.lib.makeOverridable (
|
||||
{
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
extraQtPackages ? [ ],
|
||||
}:
|
||||
pkgs.buildGoModule (
|
||||
let
|
||||
rootSrc = ./.;
|
||||
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||
in
|
||||
{
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X 'main.Version=${version}'"
|
||||
];
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X 'main.Version=${version}'"
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
|
||||
# Install desktop file and icon
|
||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||
$out/share/applications/dms-open.desktop
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
# Install desktop file and icon
|
||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||
$out/share/applications/dms-open.desktop
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$out/lib/systemd/user/dms.service
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$out/lib/systemd/user/dms.service
|
||||
|
||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
--zsh <($out/bin/dms completion zsh)
|
||||
'';
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
--zsh <($out/bin/dms completion zsh)
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||
homepage = "https://danklinux.com";
|
||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
mainProgram = "dms";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
);
|
||||
meta = {
|
||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||
homepage = "https://danklinux.com";
|
||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
mainProgram = "dms";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
)
|
||||
) { };
|
||||
|
||||
quickshell = quickshell.packages.${system}.default;
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
|
||||
|
||||
# Code formatting and linting
|
||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
|
||||
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
|
||||
```
|
||||
|
||||
@@ -783,7 +783,7 @@ When modifying the shell:
|
||||
|
||||
**QML Frontend:**
|
||||
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
||||
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` 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)
|
||||
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ Singleton {
|
||||
property bool _hasUnsavedChanges: false
|
||||
property var _loadedSessionSnapshot: null
|
||||
readonly property var _hooks: ({
|
||||
"updateLocale": updateLocale
|
||||
})
|
||||
"updateLocale": updateLocale
|
||||
})
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
@@ -1245,7 +1245,7 @@ Singleton {
|
||||
id: greeterSessionFile
|
||||
|
||||
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";
|
||||
}
|
||||
preload: isGreeterMode
|
||||
|
||||
@@ -280,6 +280,7 @@ Singleton {
|
||||
property bool showOccupiedWorkspacesOnly: false
|
||||
property bool reverseScrolling: false
|
||||
property bool dwlShowAllTags: false
|
||||
property bool workspaceActiveAppHighlightEnabled: false
|
||||
property string workspaceColorMode: "default"
|
||||
property string workspaceOccupiedColorMode: "none"
|
||||
property string workspaceUnfocusedColorMode: "default"
|
||||
@@ -313,6 +314,17 @@ Singleton {
|
||||
property string centeringMode: "index"
|
||||
property string clockDateFormat: ""
|
||||
property string lockDateFormat: ""
|
||||
property bool greeterRememberLastSession: true
|
||||
property bool greeterRememberLastUser: true
|
||||
property bool greeterEnableFprint: false
|
||||
property bool greeterEnableU2f: false
|
||||
property string greeterWallpaperPath: ""
|
||||
property bool greeterUse24HourClock: true
|
||||
property bool greeterShowSeconds: false
|
||||
property bool greeterPadHours12Hour: false
|
||||
property string greeterLockDateFormat: ""
|
||||
property string greeterFontFamily: ""
|
||||
property string greeterWallpaperFillMode: ""
|
||||
property int mediaSize: 1
|
||||
|
||||
property string appLauncherViewMode: "list"
|
||||
@@ -456,18 +468,20 @@ Singleton {
|
||||
property bool matugenTemplateGhostty: true
|
||||
property bool matugenTemplateKitty: true
|
||||
property bool matugenTemplateFoot: true
|
||||
property bool matugenTemplateNeovim: true
|
||||
property bool matugenTemplateNeovim: false
|
||||
property bool matugenTemplateAlacritty: true
|
||||
property bool matugenTemplateWezterm: true
|
||||
property bool matugenTemplateDgop: true
|
||||
property bool matugenTemplateKcolorscheme: true
|
||||
property bool matugenTemplateVscode: true
|
||||
property bool matugenTemplateEmacs: true
|
||||
property bool matugenTemplateZed: true
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
property bool dockSmartAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property bool dockRestoreSpecialWorkspaceOnClick: false
|
||||
property bool dockOpenOnOverview: false
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
property real dockSpacing: 4
|
||||
@@ -513,9 +527,23 @@ Singleton {
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 15
|
||||
property bool fprintdAvailable: false
|
||||
property bool lockFingerprintCanEnable: false
|
||||
property bool lockFingerprintReady: false
|
||||
property string lockFingerprintReason: "probe_failed"
|
||||
property bool greeterFingerprintCanEnable: false
|
||||
property bool greeterFingerprintReady: false
|
||||
property string greeterFingerprintReason: "probe_failed"
|
||||
property string greeterFingerprintSource: "none"
|
||||
property bool enableU2f: false
|
||||
property string u2fMode: "or"
|
||||
property bool u2fAvailable: false
|
||||
property bool lockU2fCanEnable: false
|
||||
property bool lockU2fReady: false
|
||||
property string lockU2fReason: "probe_failed"
|
||||
property bool greeterU2fCanEnable: false
|
||||
property bool greeterU2fReady: false
|
||||
property string greeterU2fReason: "probe_failed"
|
||||
property string greeterU2fSource: "none"
|
||||
property string lockScreenActiveMonitor: "all"
|
||||
property string lockScreenInactiveColor: "#000000"
|
||||
property int lockScreenNotificationMode: 0
|
||||
@@ -538,6 +566,7 @@ Singleton {
|
||||
property bool notificationHistorySaveNormal: true
|
||||
property bool notificationHistorySaveCritical: true
|
||||
property var notificationRules: []
|
||||
property bool notificationFocusedMonitor: false
|
||||
|
||||
property bool osdAlwaysShowValue: false
|
||||
property int osdPosition: SettingsData.Position.BottomCenter
|
||||
@@ -1001,13 +1030,19 @@ Singleton {
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
|
||||
function refreshAuthAvailability() {
|
||||
if (isGreeterMode)
|
||||
return;
|
||||
Processes.settingsRoot = root;
|
||||
Processes.detectAuthCapabilities();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
Processes.settingsRoot = root;
|
||||
loadSettings();
|
||||
initializeListModels();
|
||||
Processes.detectFprintd();
|
||||
Processes.detectU2f();
|
||||
refreshAuthAvailability();
|
||||
Processes.checkPluginSettings();
|
||||
}
|
||||
}
|
||||
@@ -1155,7 +1190,7 @@ Singleton {
|
||||
"updateCompositorLayout": updateCompositorLayout,
|
||||
"applyStoredIconTheme": applyStoredIconTheme,
|
||||
"updateBarConfigs": updateBarConfigs,
|
||||
"updateCompositorCursor": updateCompositorCursor,
|
||||
"updateCompositorCursor": updateCompositorCursor
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
@@ -1247,10 +1282,47 @@ Singleton {
|
||||
return JSON.stringify(Store.toJson(root), null, 2);
|
||||
}
|
||||
|
||||
function _resetPluginSettings() {
|
||||
_pluginParseError = false;
|
||||
pluginSettings = {};
|
||||
}
|
||||
|
||||
function _pluginSettingsErrorCode(error) {
|
||||
if (typeof error === "number")
|
||||
return error;
|
||||
if (error && typeof error === "object") {
|
||||
if (typeof error.code === "number")
|
||||
return error.code;
|
||||
if (typeof error.errno === "number")
|
||||
return error.errno;
|
||||
}
|
||||
|
||||
const msg = String(error || "").trim();
|
||||
if (/^\d+$/.test(msg))
|
||||
return Number(msg);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function _isMissingPluginSettingsError(error) {
|
||||
if (_pluginSettingsErrorCode(error) === 2)
|
||||
return true;
|
||||
|
||||
const msg = String(error || "").toLowerCase();
|
||||
return msg.indexOf("file does not exist") !== -1
|
||||
|| msg.indexOf("no such file") !== -1
|
||||
|| msg.indexOf("enoent") !== -1;
|
||||
}
|
||||
|
||||
function loadPluginSettings() {
|
||||
_pluginSettingsLoading = true;
|
||||
parsePluginSettings(pluginSettingsFile.text());
|
||||
_pluginSettingsLoading = false;
|
||||
try {
|
||||
parsePluginSettings(pluginSettingsFile.text());
|
||||
} catch (e) {
|
||||
const msg = e.message || String(e);
|
||||
if (!_isMissingPluginSettingsError(e))
|
||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
||||
_resetPluginSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function parsePluginSettings(content) {
|
||||
@@ -2686,6 +2758,7 @@ Singleton {
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
printErrors: false
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
@@ -2694,7 +2767,10 @@ Singleton {
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
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: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return GreetdSettings.fontFamily;
|
||||
return GreetdSettings.getEffectiveFontFamily();
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||
}
|
||||
@@ -1551,7 +1551,7 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const skipTemplates = [];
|
||||
if (!SettingsData.runDmsMatugenTemplates) {
|
||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
|
||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
|
||||
} else {
|
||||
if (!SettingsData.matugenTemplateGtk)
|
||||
skipTemplates.push("gtk");
|
||||
@@ -1595,6 +1595,8 @@ Singleton {
|
||||
skipTemplates.push("vscode");
|
||||
if (!SettingsData.matugenTemplateEmacs)
|
||||
skipTemplates.push("emacs");
|
||||
if (!SettingsData.matugenTemplateZed)
|
||||
skipTemplates.push("zed");
|
||||
}
|
||||
if (skipTemplates.length > 0) {
|
||||
args.push("--skip-templates", skipTemplates.join(","));
|
||||
@@ -1987,7 +1989,7 @@ Singleton {
|
||||
FileView {
|
||||
id: dynamicColorsFileView
|
||||
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";
|
||||
return colorsPath;
|
||||
}
|
||||
|
||||
@@ -10,22 +10,352 @@ Singleton {
|
||||
|
||||
property var settingsRoot: null
|
||||
|
||||
property string greetdPamText: ""
|
||||
property string systemAuthPamText: ""
|
||||
property string commonAuthPamText: ""
|
||||
property string passwordAuthPamText: ""
|
||||
property string systemLoginPamText: ""
|
||||
property string systemLocalLoginPamText: ""
|
||||
property string commonAuthPcPamText: ""
|
||||
property string loginPamText: ""
|
||||
property string dankshellU2fPamText: ""
|
||||
property string u2fKeysText: ""
|
||||
|
||||
property string fingerprintProbeOutput: ""
|
||||
property int fingerprintProbeExitCode: 0
|
||||
property bool fingerprintProbeStreamFinished: false
|
||||
property bool fingerprintProbeExited: false
|
||||
property string fingerprintProbeState: "probe_failed"
|
||||
|
||||
property string pamSupportProbeOutput: ""
|
||||
property bool pamSupportProbeStreamFinished: false
|
||||
property bool pamSupportProbeExited: false
|
||||
property int pamSupportProbeExitCode: 0
|
||||
property bool pamFprintSupportDetected: false
|
||||
property bool pamU2fSupportDetected: false
|
||||
|
||||
readonly property string homeDir: Quickshell.env("HOME") || ""
|
||||
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
|
||||
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
|
||||
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
|
||||
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
|
||||
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||
|
||||
function envFlag(name) {
|
||||
const value = (Quickshell.env(name) || "").trim().toLowerCase();
|
||||
if (value === "1" || value === "true" || value === "yes" || value === "on")
|
||||
return true;
|
||||
if (value === "0" || value === "false" || value === "no" || value === "off")
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
||||
|
||||
function detectQtTools() {
|
||||
qtToolsDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function detectAuthCapabilities() {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
|
||||
if (forcedFprintAvailable === null) {
|
||||
fingerprintProbeOutput = "";
|
||||
fingerprintProbeStreamFinished = false;
|
||||
fingerprintProbeExited = false;
|
||||
fingerprintProbeProcess.running = true;
|
||||
} else {
|
||||
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||
}
|
||||
|
||||
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
|
||||
pamFprintSupportDetected = false;
|
||||
pamU2fSupportDetected = false;
|
||||
pamSupportProbeOutput = "";
|
||||
pamSupportProbeStreamFinished = false;
|
||||
pamSupportProbeExited = false;
|
||||
pamSupportDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
recomputeAuthCapabilities();
|
||||
}
|
||||
|
||||
function detectFprintd() {
|
||||
fprintdDetectionProcess.running = true;
|
||||
detectAuthCapabilities();
|
||||
}
|
||||
|
||||
function detectU2f() {
|
||||
u2fDetectionProcess.running = true;
|
||||
detectAuthCapabilities();
|
||||
}
|
||||
|
||||
function checkPluginSettings() {
|
||||
pluginSettingsCheckProcess.running = true;
|
||||
}
|
||||
|
||||
function stripPamComment(line) {
|
||||
if (!line)
|
||||
return "";
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#"))
|
||||
return "";
|
||||
const hashIdx = trimmed.indexOf("#");
|
||||
if (hashIdx >= 0)
|
||||
return trimmed.substring(0, hashIdx).trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function pamModuleEnabled(pamText, moduleName) {
|
||||
if (!pamText || !moduleName)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
if (line.includes(moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function pamTextIncludesFile(pamText, filename) {
|
||||
if (!pamText || !filename)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function greeterPamStackHasModule(moduleName) {
|
||||
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||
return true;
|
||||
const includedPamStacks = [
|
||||
["system-auth", systemAuthPamText],
|
||||
["common-auth", commonAuthPamText],
|
||||
["password-auth", passwordAuthPamText],
|
||||
["system-login", systemLoginPamText],
|
||||
["system-local-login", systemLocalLoginPamText],
|
||||
["common-auth-pc", commonAuthPcPamText],
|
||||
["login", loginPamText]
|
||||
];
|
||||
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||
const stack = includedPamStacks[i];
|
||||
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasEnrolledFingerprintOutput(output) {
|
||||
const lower = (output || "").toLowerCase();
|
||||
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
|
||||
return true;
|
||||
const lines = lower.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim();
|
||||
if (trimmed.startsWith("finger:"))
|
||||
return true;
|
||||
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasMissingFingerprintEnrollmentOutput(output) {
|
||||
const lower = (output || "").toLowerCase();
|
||||
return lower.includes("no fingers enrolled")
|
||||
|| lower.includes("no fingerprints enrolled")
|
||||
|| lower.includes("no prints enrolled");
|
||||
}
|
||||
|
||||
function hasMissingFingerprintReaderOutput(output) {
|
||||
const lower = (output || "").toLowerCase();
|
||||
return lower.includes("no devices available")
|
||||
|| lower.includes("no device available")
|
||||
|| lower.includes("no devices found")
|
||||
|| lower.includes("list_devices failed")
|
||||
|| lower.includes("no device");
|
||||
}
|
||||
|
||||
function parseFingerprintProbe(exitCode, output) {
|
||||
if (hasEnrolledFingerprintOutput(output))
|
||||
return "ready";
|
||||
if (hasMissingFingerprintEnrollmentOutput(output))
|
||||
return "missing_enrollment";
|
||||
if (hasMissingFingerprintReaderOutput(output))
|
||||
return "missing_reader";
|
||||
if (exitCode === 0)
|
||||
return "missing_enrollment";
|
||||
if (exitCode === 127 || (output || "").includes("__missing_command__"))
|
||||
return "probe_failed";
|
||||
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
|
||||
}
|
||||
|
||||
function setLockFingerprintCapability(canEnable, ready, reason) {
|
||||
settingsRoot.lockFingerprintCanEnable = canEnable;
|
||||
settingsRoot.lockFingerprintReady = ready;
|
||||
settingsRoot.lockFingerprintReason = reason;
|
||||
}
|
||||
|
||||
function setLockU2fCapability(canEnable, ready, reason) {
|
||||
settingsRoot.lockU2fCanEnable = canEnable;
|
||||
settingsRoot.lockU2fReady = ready;
|
||||
settingsRoot.lockU2fReason = reason;
|
||||
}
|
||||
|
||||
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
|
||||
settingsRoot.greeterFingerprintCanEnable = canEnable;
|
||||
settingsRoot.greeterFingerprintReady = ready;
|
||||
settingsRoot.greeterFingerprintReason = reason;
|
||||
settingsRoot.greeterFingerprintSource = source;
|
||||
}
|
||||
|
||||
function setGreeterU2fCapability(canEnable, ready, reason, source) {
|
||||
settingsRoot.greeterU2fCanEnable = canEnable;
|
||||
settingsRoot.greeterU2fReady = ready;
|
||||
settingsRoot.greeterU2fReason = reason;
|
||||
settingsRoot.greeterU2fSource = source;
|
||||
}
|
||||
|
||||
function recomputeFingerprintCapabilities() {
|
||||
if (forcedFprintAvailable !== null) {
|
||||
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||
const source = forcedFprintAvailable ? "dms" : "none";
|
||||
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
|
||||
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = fingerprintProbeState;
|
||||
|
||||
switch (state) {
|
||||
case "ready":
|
||||
setLockFingerprintCapability(true, true, "ready");
|
||||
break;
|
||||
case "missing_enrollment":
|
||||
setLockFingerprintCapability(true, false, "missing_enrollment");
|
||||
break;
|
||||
case "missing_reader":
|
||||
setLockFingerprintCapability(false, false, "missing_reader");
|
||||
break;
|
||||
case "missing_pam_support":
|
||||
setLockFingerprintCapability(false, false, "missing_pam_support");
|
||||
break;
|
||||
default:
|
||||
setLockFingerprintCapability(false, false, "probe_failed");
|
||||
break;
|
||||
}
|
||||
|
||||
if (greeterPamHasFprint) {
|
||||
switch (state) {
|
||||
case "ready":
|
||||
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
|
||||
break;
|
||||
case "missing_enrollment":
|
||||
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
|
||||
break;
|
||||
case "missing_reader":
|
||||
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
|
||||
break;
|
||||
default:
|
||||
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case "ready":
|
||||
setGreeterFingerprintCapability(true, true, "ready", "dms");
|
||||
break;
|
||||
case "missing_enrollment":
|
||||
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
|
||||
break;
|
||||
case "missing_reader":
|
||||
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
|
||||
break;
|
||||
case "missing_pam_support":
|
||||
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
|
||||
break;
|
||||
default:
|
||||
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function recomputeU2fCapabilities() {
|
||||
if (forcedU2fAvailable !== null) {
|
||||
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
|
||||
const source = forcedU2fAvailable ? "dms" : "none";
|
||||
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
|
||||
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
|
||||
return;
|
||||
}
|
||||
|
||||
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
|
||||
const lockCanEnable = lockReady || pamU2fSupportDetected;
|
||||
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
|
||||
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
|
||||
|
||||
if (greeterPamHasU2f) {
|
||||
setGreeterU2fCapability(true, true, "configured_externally", "pam");
|
||||
return;
|
||||
}
|
||||
|
||||
const greeterReady = homeU2fKeysDetected;
|
||||
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
|
||||
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
|
||||
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
|
||||
}
|
||||
|
||||
function recomputeAuthCapabilities() {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
recomputeFingerprintCapabilities();
|
||||
recomputeU2fCapabilities();
|
||||
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
|
||||
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
|
||||
}
|
||||
|
||||
function finalizeFingerprintProbe() {
|
||||
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
|
||||
return;
|
||||
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
||||
recomputeAuthCapabilities();
|
||||
}
|
||||
|
||||
function finalizePamSupportProbe() {
|
||||
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
|
||||
return;
|
||||
|
||||
pamFprintSupportDetected = false;
|
||||
pamU2fSupportDetected = false;
|
||||
|
||||
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parts = lines[i].split(":");
|
||||
if (parts.length !== 2)
|
||||
continue;
|
||||
if (parts[0] === "pam_fprintd.so")
|
||||
pamFprintSupportDetected = parts[1] === "true";
|
||||
else if (parts[0] === "pam_u2f.so")
|
||||
pamU2fSupportDetected = parts[1] === "true";
|
||||
}
|
||||
|
||||
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
|
||||
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
||||
|
||||
recomputeAuthCapabilities();
|
||||
}
|
||||
|
||||
property var qtToolsDetectionProcess: Process {
|
||||
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
||||
running: false
|
||||
@@ -35,15 +365,15 @@ Singleton {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
if (text && text.trim()) {
|
||||
var lines = text.trim().split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
if (line.startsWith('qt5ct:')) {
|
||||
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
|
||||
} else if (line.startsWith('qt6ct:')) {
|
||||
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
|
||||
} else if (line.startsWith('gtk:')) {
|
||||
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
|
||||
const lines = text.trim().split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith("qt5ct:")) {
|
||||
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
|
||||
} else if (line.startsWith("qt6ct:")) {
|
||||
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
|
||||
} else if (line.startsWith("gtk:")) {
|
||||
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,23 +381,181 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
property var fprintdDetectionProcess: Process {
|
||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
||||
property var fingerprintProbeProcess: Process {
|
||||
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.fingerprintProbeOutput = text || "";
|
||||
root.fingerprintProbeStreamFinished = true;
|
||||
root.finalizeFingerprintProbe();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
settingsRoot.fprintdAvailable = (exitCode === 0);
|
||||
root.fingerprintProbeExitCode = exitCode;
|
||||
root.fingerprintProbeExited = true;
|
||||
root.finalizeFingerprintProbe();
|
||||
}
|
||||
}
|
||||
|
||||
property var u2fDetectionProcess: Process {
|
||||
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
||||
property var pamSupportDetectionProcess: Process {
|
||||
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.pamSupportProbeOutput = text || "";
|
||||
root.pamSupportProbeStreamFinished = true;
|
||||
root.finalizePamSupportProbe();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
settingsRoot.u2fAvailable = (exitCode === 0);
|
||||
root.pamSupportProbeExitCode = exitCode;
|
||||
root.pamSupportProbeExited = true;
|
||||
root.finalizePamSupportProbe();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: greetdPamWatcher
|
||||
path: "/etc/pam.d/greetd"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.greetdPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.greetdPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemAuthPamWatcher
|
||||
path: "/etc/pam.d/system-auth"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.systemAuthPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.systemAuthPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: commonAuthPamWatcher
|
||||
path: "/etc/pam.d/common-auth"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.commonAuthPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.commonAuthPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: passwordAuthPamWatcher
|
||||
path: "/etc/pam.d/password-auth"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.passwordAuthPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.passwordAuthPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemLoginPamWatcher
|
||||
path: "/etc/pam.d/system-login"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.systemLoginPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.systemLoginPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemLocalLoginPamWatcher
|
||||
path: "/etc/pam.d/system-local-login"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.systemLocalLoginPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.systemLocalLoginPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: commonAuthPcPamWatcher
|
||||
path: "/etc/pam.d/common-auth-pc"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.commonAuthPcPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.commonAuthPcPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: loginPamWatcher
|
||||
path: "/etc/pam.d/login"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.loginPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.loginPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: dankshellU2fPamWatcher
|
||||
path: "/etc/pam.d/dankshell-u2f"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.dankshellU2fPamText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.dankshellU2fPamText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: u2fKeysWatcher
|
||||
path: root.u2fKeysPath
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.u2fKeysText = text();
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.u2fKeysText = "";
|
||||
root.recomputeAuthCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ var SPEC = {
|
||||
showOccupiedWorkspacesOnly: { def: false },
|
||||
reverseScrolling: { def: false },
|
||||
dwlShowAllTags: { def: false },
|
||||
workspaceActiveAppHighlightEnabled: { def: false },
|
||||
workspaceColorMode: { def: "default" },
|
||||
workspaceOccupiedColorMode: { def: "none" },
|
||||
workspaceUnfocusedColorMode: { def: "default" },
|
||||
@@ -164,6 +165,17 @@ var SPEC = {
|
||||
centeringMode: { def: "index" },
|
||||
clockDateFormat: { def: "" },
|
||||
lockDateFormat: { def: "" },
|
||||
greeterRememberLastSession: { def: true },
|
||||
greeterRememberLastUser: { def: true },
|
||||
greeterEnableFprint: { def: false },
|
||||
greeterEnableU2f: { def: false },
|
||||
greeterWallpaperPath: { def: "" },
|
||||
greeterUse24HourClock: { def: true },
|
||||
greeterShowSeconds: { def: false },
|
||||
greeterPadHours12Hour: { def: false },
|
||||
greeterLockDateFormat: { def: "" },
|
||||
greeterFontFamily: { def: "" },
|
||||
greeterWallpaperFillMode: { def: "" },
|
||||
mediaSize: { def: 1 },
|
||||
|
||||
appLauncherViewMode: { def: "list" },
|
||||
@@ -272,17 +284,19 @@ var SPEC = {
|
||||
matugenTemplateKitty: { def: true },
|
||||
matugenTemplateFoot: { def: true },
|
||||
matugenTemplateAlacritty: { def: true },
|
||||
matugenTemplateNeovim: { def: true },
|
||||
matugenTemplateNeovim: { def: false },
|
||||
matugenTemplateWezterm: { def: true },
|
||||
matugenTemplateDgop: { def: true },
|
||||
matugenTemplateKcolorscheme: { def: true },
|
||||
matugenTemplateVscode: { def: true },
|
||||
matugenTemplateEmacs: { def: true },
|
||||
matugenTemplateZed: { def: true },
|
||||
|
||||
showDock: { def: false },
|
||||
dockAutoHide: { def: false },
|
||||
dockSmartAutoHide: { def: false },
|
||||
dockGroupByApp: { def: false },
|
||||
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
||||
dockOpenOnOverview: { def: false },
|
||||
dockPosition: { def: 1 },
|
||||
dockSpacing: { def: 4 },
|
||||
@@ -327,9 +341,23 @@ var SPEC = {
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
fprintdAvailable: { def: false, persist: false },
|
||||
lockFingerprintCanEnable: { def: false, persist: false },
|
||||
lockFingerprintReady: { def: false, persist: false },
|
||||
lockFingerprintReason: { def: "probe_failed", persist: false },
|
||||
greeterFingerprintCanEnable: { def: false, persist: false },
|
||||
greeterFingerprintReady: { def: false, persist: false },
|
||||
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
||||
greeterFingerprintSource: { def: "none", persist: false },
|
||||
enableU2f: { def: false },
|
||||
u2fMode: { def: "or" },
|
||||
u2fAvailable: { def: false, persist: false },
|
||||
lockU2fCanEnable: { def: false, persist: false },
|
||||
lockU2fReady: { def: false, persist: false },
|
||||
lockU2fReason: { def: "probe_failed", persist: false },
|
||||
greeterU2fCanEnable: { def: false, persist: false },
|
||||
greeterU2fReady: { def: false, persist: false },
|
||||
greeterU2fReason: { def: "probe_failed", persist: false },
|
||||
greeterU2fSource: { def: "none", persist: false },
|
||||
lockScreenActiveMonitor: { def: "all" },
|
||||
lockScreenInactiveColor: { def: "#000000" },
|
||||
lockScreenNotificationMode: { def: 0 },
|
||||
@@ -352,6 +380,7 @@ var SPEC = {
|
||||
notificationHistorySaveNormal: { def: true },
|
||||
notificationHistorySaveCritical: { def: true },
|
||||
notificationRules: { def: [] },
|
||||
notificationFocusedMonitor: { def: false },
|
||||
|
||||
osdAlwaysShowValue: { def: false },
|
||||
osdPosition: { def: 5 },
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Greetd
|
||||
import qs.Common
|
||||
import qs.Modules.Greetd
|
||||
|
||||
Scope {
|
||||
|
||||
@@ -313,7 +313,7 @@ Item {
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("notifications")
|
||||
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
|
||||
|
||||
delegate: NotificationPopupManager {
|
||||
modelData: item
|
||||
|
||||
@@ -1006,9 +1006,7 @@ Item {
|
||||
_applyHighlights(newSections, searchQuery);
|
||||
flatModel = Scorer.flattenSections(newSections);
|
||||
sections = newSections;
|
||||
if (selectedFlatIndex >= flatModel.length) {
|
||||
selectedFlatIndex = getFirstItemIndex();
|
||||
}
|
||||
selectedFlatIndex = getFirstItemIndex();
|
||||
updateSelectedItem();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ Item {
|
||||
switch (mode) {
|
||||
case "files":
|
||||
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)
|
||||
return I18n.tr("Type to search files");
|
||||
if (root.controller.searchQuery.length < 2)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -11,8 +12,45 @@ FloatingWindow {
|
||||
property string passwordInput: ""
|
||||
property var currentFlow: PolkitService.agent?.flow
|
||||
property bool isLoading: false
|
||||
property bool awaitingFprintForPassword: false
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
|
||||
property string polkitEtcPamText: ""
|
||||
property string polkitLibPamText: ""
|
||||
property string systemAuthPamText: ""
|
||||
property string commonAuthPamText: ""
|
||||
property string passwordAuthPamText: ""
|
||||
readonly property bool polkitPamHasFprint: {
|
||||
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||
if (!polkitText)
|
||||
return false;
|
||||
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||
}
|
||||
|
||||
function stripPamComment(line) {
|
||||
if (!line)
|
||||
return "";
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#"))
|
||||
return "";
|
||||
const hashIdx = trimmed.indexOf("#");
|
||||
if (hashIdx >= 0)
|
||||
return trimmed.substring(0, hashIdx).trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function pamModuleEnabled(pamText, moduleName) {
|
||||
if (!pamText || !moduleName)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (line && line.includes(moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function focusPasswordField() {
|
||||
passwordField.forceActiveFocus();
|
||||
}
|
||||
@@ -20,6 +58,7 @@ FloatingWindow {
|
||||
function show() {
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
visible = true;
|
||||
Qt.callLater(focusPasswordField);
|
||||
}
|
||||
@@ -28,17 +67,27 @@ FloatingWindow {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function _commitSubmit() {
|
||||
isLoading = true;
|
||||
awaitingFprintForPassword = false;
|
||||
currentFlow.submit(passwordInput);
|
||||
passwordInput = "";
|
||||
}
|
||||
|
||||
function submitAuth() {
|
||||
if (!currentFlow || isLoading)
|
||||
return;
|
||||
isLoading = true;
|
||||
currentFlow.submit(passwordInput);
|
||||
passwordInput = "";
|
||||
if (!currentFlow.isResponseRequired) {
|
||||
awaitingFprintForPassword = true;
|
||||
return;
|
||||
}
|
||||
_commitSubmit();
|
||||
}
|
||||
|
||||
function cancelAuth() {
|
||||
if (isLoading)
|
||||
return;
|
||||
awaitingFprintForPassword = false;
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest();
|
||||
return;
|
||||
@@ -60,6 +109,7 @@ FloatingWindow {
|
||||
}
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -83,6 +133,11 @@ FloatingWindow {
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (!currentFlow.isResponseRequired)
|
||||
return;
|
||||
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||
_commitSubmit();
|
||||
return;
|
||||
}
|
||||
awaitingFprintForPassword = false;
|
||||
isLoading = false;
|
||||
passwordInput = "";
|
||||
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 {
|
||||
id: contentFocusScope
|
||||
|
||||
@@ -205,36 +295,30 @@ FloatingWindow {
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
width: parent.width
|
||||
height: inputFieldHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: passwordField.activeFocus ? 2 : 1
|
||||
backgroundColor: Theme.surfaceHover
|
||||
normalBorderColor: Theme.outlineStrong
|
||||
focusedBorderColor: Theme.primary
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||
leftIconSize: 20
|
||||
leftIconColor: Theme.primary
|
||||
leftIconFocusedColor: Theme.primary
|
||||
opacity: isLoading ? 0.5 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !isLoading
|
||||
onClicked: passwordField.forceActiveFocus()
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: passwordInput
|
||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
backgroundColor: "transparent"
|
||||
enabled: !isLoading
|
||||
onTextEdited: passwordInput = text
|
||||
onAccepted: submitAuth()
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: passwordInput
|
||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
enabled: !isLoading
|
||||
onTextEdited: passwordInput = text
|
||||
onAccepted: submitAuth()
|
||||
}
|
||||
|
||||
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 {
|
||||
id: pluginsLoader
|
||||
anchors.fill: parent
|
||||
@@ -470,7 +485,7 @@ FocusScope {
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +500,7 @@ FocusScope {
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,12 @@ Rectangle {
|
||||
"icon": "lock",
|
||||
"tabIndex": 11
|
||||
},
|
||||
{
|
||||
"id": "greeter",
|
||||
"text": I18n.tr("Greeter"),
|
||||
"icon": "login",
|
||||
"tabIndex": 31
|
||||
},
|
||||
{
|
||||
"id": "power_sleep",
|
||||
"text": I18n.tr("Power & Sleep"),
|
||||
|
||||
@@ -8,6 +8,9 @@ DankPopout {
|
||||
|
||||
layerNamespace: "dms:app-launcher"
|
||||
|
||||
readonly property real screenWidth: screen?.width ?? 1920
|
||||
readonly property real screenHeight: screen?.height ?? 1080
|
||||
|
||||
property string _pendingMode: ""
|
||||
property string _pendingQuery: ""
|
||||
|
||||
@@ -41,8 +44,35 @@ DankPopout {
|
||||
openWithQuery(query);
|
||||
}
|
||||
|
||||
popupWidth: 560
|
||||
popupHeight: 640
|
||||
readonly property int _baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int _baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: Math.min(_baseWidth, screenWidth - 100)
|
||||
popupHeight: Math.min(_baseHeight, screenHeight - 100)
|
||||
|
||||
triggerWidth: 40
|
||||
positioning: ""
|
||||
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||
|
||||
@@ -12,7 +12,7 @@ DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:control-center"
|
||||
fullHeightSurface: false
|
||||
fullHeightSurface: true
|
||||
|
||||
property string expandedSection: ""
|
||||
property var triggerScreen: null
|
||||
|
||||
@@ -390,10 +390,11 @@ BasePill {
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: audioPercentV
|
||||
visible: root.showAudioPercent
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -416,10 +417,11 @@ BasePill {
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: micPercentV
|
||||
visible: root.showMicPercent
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -442,10 +444,11 @@ BasePill {
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: brightnessPercentV
|
||||
visible: root.showBrightnessPercent
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -536,7 +539,8 @@ BasePill {
|
||||
}
|
||||
|
||||
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
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -552,20 +556,23 @@ BasePill {
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: audioPercent
|
||||
visible: root.showAudioPercent
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: audioIcon.right
|
||||
anchors.leftMargin: 2
|
||||
width: reservedWidth
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -581,20 +588,22 @@ BasePill {
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: micPercent
|
||||
visible: root.showMicPercent
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: micIcon.right
|
||||
anchors.leftMargin: 2
|
||||
width: reservedWidth
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -610,15 +619,17 @@ BasePill {
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: brightnessPercent
|
||||
visible: root.showBrightnessPercent
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: brightnessIcon.right
|
||||
anchors.leftMargin: 2
|
||||
width: reservedWidth
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,16 +211,17 @@ BasePill {
|
||||
text: {
|
||||
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
||||
const appName = appText.text;
|
||||
|
||||
if (compactMode && title === appName) {
|
||||
return title;
|
||||
}
|
||||
|
||||
if (!title || !appName) {
|
||||
return title;
|
||||
}
|
||||
|
||||
if (title.endsWith(" - " + appName)) {
|
||||
return title.substring(0, title.length - (" - " + appName).length);
|
||||
}
|
||||
|
||||
if (title.endsWith(appName)) {
|
||||
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
|
||||
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
|
||||
}
|
||||
|
||||
return title;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.SystemTray
|
||||
@@ -162,6 +161,23 @@ BasePill {
|
||||
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 var currentTrayMenu: null
|
||||
|
||||
@@ -940,13 +956,6 @@ BasePill {
|
||||
}
|
||||
})(), 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
|
||||
scale: root.menuOpen ? 1 : 0.85
|
||||
|
||||
@@ -967,19 +976,14 @@ BasePill {
|
||||
ElevationShadow {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
level: menuContainer.elev
|
||||
fallbackOffset: 4
|
||||
shadowBlurPx: menuContainer.shadowBlurPx
|
||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
||||
shadowColor: {
|
||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
||||
}
|
||||
level: Theme.elevationLevel3
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: 6
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
targetRadius: Theme.cornerRadius
|
||||
sourceRect.antialiasing: true
|
||||
sourceRect.smooth: true
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
@@ -1402,13 +1406,6 @@ BasePill {
|
||||
}
|
||||
})(), 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
|
||||
scale: menuRoot.showMenu ? 1 : 0.85
|
||||
|
||||
@@ -1429,18 +1426,13 @@ BasePill {
|
||||
ElevationShadow {
|
||||
id: menuBgShadowLayer
|
||||
anchors.fill: parent
|
||||
level: menuContainer.elev
|
||||
fallbackOffset: 4
|
||||
shadowBlurPx: menuContainer.shadowBlurPx
|
||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
||||
shadowColor: {
|
||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
||||
}
|
||||
level: Theme.elevationLevel3
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: 6
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
targetRadius: Theme.cornerRadius
|
||||
sourceRect.antialiasing: true
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
@@ -1470,6 +1470,10 @@ Item {
|
||||
delegate: Item {
|
||||
width: 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 {
|
||||
id: rowAppIcon
|
||||
@@ -1485,14 +1489,14 @@ Item {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
border.color: appBorderColor
|
||||
opacity: appOpacity
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||
font.pixelSize: parent.width * 0.45
|
||||
color: Theme.primary
|
||||
color: appGlyphColor
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
@@ -1503,14 +1507,14 @@ Item {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
border.color: appBorderColor
|
||||
opacity: appOpacity
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: parent.width * 0.7
|
||||
name: "sports_esports"
|
||||
color: Theme.primary
|
||||
color: appGlyphColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1523,11 +1527,12 @@ Item {
|
||||
layer.effect: MultiEffect {
|
||||
saturation: 0
|
||||
colorization: 1
|
||||
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
|
||||
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: rowSteamIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
@@ -1538,11 +1543,21 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
size: root.appIconSize
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
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 {
|
||||
id: rowAppMouseArea
|
||||
anchors.fill: parent
|
||||
@@ -1624,6 +1639,10 @@ Item {
|
||||
delegate: Item {
|
||||
width: 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 {
|
||||
id: colAppIcon
|
||||
@@ -1639,14 +1658,14 @@ Item {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
border.color: appBorderColor
|
||||
opacity: appOpacity
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||
font.pixelSize: parent.width * 0.45
|
||||
color: Theme.primary
|
||||
color: appGlyphColor
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
@@ -1657,14 +1676,14 @@ Item {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
border.color: appBorderColor
|
||||
opacity: appOpacity
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: parent.width * 0.7
|
||||
name: "sports_esports"
|
||||
color: Theme.primary
|
||||
color: appGlyphColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1677,11 +1696,12 @@ Item {
|
||||
layer.effect: MultiEffect {
|
||||
saturation: 0
|
||||
colorization: 1
|
||||
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
|
||||
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: colSteamIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
@@ -1692,11 +1712,21 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
size: root.appIconSize
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
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 {
|
||||
id: colAppMouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
@@ -133,6 +134,40 @@ Item {
|
||||
function getGroupedToplevels() {
|
||||
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: {
|
||||
if (mouseArea.pressed || dragging)
|
||||
return;
|
||||
@@ -276,8 +311,11 @@ Item {
|
||||
break;
|
||||
case "window":
|
||||
const windowToplevel = getToplevelObject();
|
||||
if (windowToplevel)
|
||||
if (windowToplevel) {
|
||||
if (restoreSpecialWorkspaceWindow(windowToplevel))
|
||||
return;
|
||||
windowToplevel.activate();
|
||||
}
|
||||
break;
|
||||
case "grouped":
|
||||
if (appData.windowCount === 0) {
|
||||
@@ -300,8 +338,11 @@ Item {
|
||||
SessionService.launchDesktopEntry(groupedEntry);
|
||||
} else if (appData.windowCount === 1) {
|
||||
const groupedToplevel = getToplevelObject();
|
||||
if (groupedToplevel)
|
||||
if (groupedToplevel) {
|
||||
if (restoreSpecialWorkspaceWindow(groupedToplevel))
|
||||
return;
|
||||
groupedToplevel.activate();
|
||||
}
|
||||
} else if (contextMenu) {
|
||||
const shouldHidePin = appData.appId === "org.quickshell";
|
||||
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 Quickshell
|
||||
import Quickshell.Io
|
||||
import "GreetdEnv.js" as GreetdEnv
|
||||
|
||||
Singleton {
|
||||
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 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 lastSuccessfulUser: ""
|
||||
@@ -49,26 +52,44 @@ Singleton {
|
||||
if (!content || !content.trim())
|
||||
return;
|
||||
const memory = JSON.parse(content);
|
||||
lastSessionId = memory.lastSessionId || "";
|
||||
lastSuccessfulUser = memory.lastSuccessfulUser || "";
|
||||
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
||||
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
||||
if (!rememberLastSession || !rememberLastUser)
|
||||
saveMemory();
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse greetd memory:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function saveMemory() {
|
||||
memoryFileView.setText(JSON.stringify({
|
||||
"lastSessionId": lastSessionId,
|
||||
"lastSuccessfulUser": lastSuccessfulUser
|
||||
}, null, 2));
|
||||
let memory = {};
|
||||
if (rememberLastSession && lastSessionId)
|
||||
memory.lastSessionId = lastSessionId;
|
||||
if (rememberLastUser && lastSuccessfulUser)
|
||||
memory.lastSuccessfulUser = lastSuccessfulUser;
|
||||
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
||||
}
|
||||
|
||||
function setLastSessionId(id) {
|
||||
if (!rememberLastSession) {
|
||||
if (lastSessionId !== "") {
|
||||
lastSessionId = "";
|
||||
saveMemory();
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastSessionId = id || "";
|
||||
saveMemory();
|
||||
}
|
||||
|
||||
function setLastSuccessfulUser(username) {
|
||||
if (!rememberLastUser) {
|
||||
if (lastSuccessfulUser !== "") {
|
||||
lastSuccessfulUser = "";
|
||||
saveMemory();
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastSuccessfulUser = username || "";
|
||||
saveMemory();
|
||||
}
|
||||
|
||||
@@ -5,15 +5,22 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import "GreetdEnv.js" as GreetdEnv
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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 bool settingsLoaded: false
|
||||
property string customThemeFile: ""
|
||||
@@ -21,6 +28,12 @@ Singleton {
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: 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 nightModeEnabled: false
|
||||
property string weatherLocation: "New York, NY"
|
||||
@@ -41,6 +54,11 @@ Singleton {
|
||||
property string lockDateFormat: ""
|
||||
property bool lockScreenShowPowerActions: 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 real powerActionHoldDuration: 0.5
|
||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
@@ -52,66 +70,103 @@ Singleton {
|
||||
|
||||
function parseSettings(content) {
|
||||
try {
|
||||
let settings = {};
|
||||
if (content && content.trim()) {
|
||||
const settings = JSON.parse(content);
|
||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
||||
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
||||
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
||||
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
||||
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
||||
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
||||
settingsLoaded = true;
|
||||
settings = JSON.parse(content);
|
||||
}
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
if (currentThemeName === "custom" && customThemeFile) {
|
||||
Theme.loadCustomThemeFromFile(customThemeFile);
|
||||
}
|
||||
Theme.applyGreeterTheme(currentThemeName);
|
||||
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
|
||||
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
|
||||
|
||||
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;
|
||||
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) {
|
||||
console.warn("Failed to parse greetd settings:", e);
|
||||
} finally {
|
||||
settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock)
|
||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
||||
if (padHours12Hour)
|
||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||
const use24 = greeterUse24HourClock;
|
||||
const secs = greeterShowSeconds;
|
||||
const pad = greeterPadHours12Hour;
|
||||
if (use24)
|
||||
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() {
|
||||
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) {
|
||||
@@ -133,5 +188,9 @@ Singleton {
|
||||
onLoaded: {
|
||||
parseSettings(settingsFile.text());
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
console.warn("Failed to load greetd settings:", error);
|
||||
root.parseSettings("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,35 @@ Item {
|
||||
signal launchRequested
|
||||
|
||||
property bool weatherInitialized: false
|
||||
property bool awaitingExternalAuth: false
|
||||
property bool pendingPasswordResponse: false
|
||||
property bool passwordSubmitRequested: false
|
||||
property bool cancelingExternalAuthForPassword: false
|
||||
property int defaultAuthTimeoutMs: 10000
|
||||
property int externalAuthTimeoutMs: 30000
|
||||
property int memoryFlushDelayMs: 120
|
||||
property string pendingLaunchCommand: ""
|
||||
property var pendingLaunchEnv: []
|
||||
property int passwordFailureCount: 0
|
||||
property int passwordAttemptLimitHint: 0
|
||||
property string authFeedbackMessage: ""
|
||||
property string greetdPamText: ""
|
||||
property string systemAuthPamText: ""
|
||||
property string commonAuthPamText: ""
|
||||
property string passwordAuthPamText: ""
|
||||
property string systemLoginPamText: ""
|
||||
property string systemLocalLoginPamText: ""
|
||||
property string commonAuthPcPamText: ""
|
||||
property string loginPamText: ""
|
||||
property string faillockConfigText: ""
|
||||
property bool greeterWallpaperOverrideExists: false
|
||||
property string externalAuthAutoStartedForUser: ""
|
||||
property int passwordSessionTransitionRetryCount: 0
|
||||
property int maxPasswordSessionTransitionRetries: 2
|
||||
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
|
||||
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
|
||||
|
||||
function initWeatherService() {
|
||||
if (weatherInitialized)
|
||||
@@ -44,16 +73,357 @@ Item {
|
||||
WeatherService.forceRefresh();
|
||||
}
|
||||
|
||||
function stripPamComment(line) {
|
||||
if (!line)
|
||||
return "";
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#"))
|
||||
return "";
|
||||
const hashIdx = trimmed.indexOf("#");
|
||||
if (hashIdx >= 0)
|
||||
return trimmed.substring(0, hashIdx).trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function pamModuleEnabled(pamText, moduleName) {
|
||||
if (!pamText || !moduleName)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
if (line.includes(moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function pamTextIncludesFile(pamText, filename) {
|
||||
if (!pamText || !filename)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function greeterPamStackHasModule(moduleName) {
|
||||
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||
return true;
|
||||
const includedPamStacks = [
|
||||
["system-auth", systemAuthPamText],
|
||||
["common-auth", commonAuthPamText],
|
||||
["password-auth", passwordAuthPamText],
|
||||
["system-login", systemLoginPamText],
|
||||
["system-local-login", systemLocalLoginPamText],
|
||||
["common-auth-pc", commonAuthPcPamText],
|
||||
["login", loginPamText]
|
||||
];
|
||||
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||
const stack = includedPamStacks[i];
|
||||
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function usesPamLockoutPolicy(pamText) {
|
||||
if (!pamText)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function parsePamLineDenyValue(pamText) {
|
||||
if (!pamText)
|
||||
return -1;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
|
||||
continue;
|
||||
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
|
||||
if (!denyMatch)
|
||||
continue;
|
||||
const parsed = parseInt(denyMatch[1], 10);
|
||||
if (!isNaN(parsed))
|
||||
return parsed;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseFaillockDenyValue(configText) {
|
||||
if (!configText)
|
||||
return -1;
|
||||
const lines = configText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (!line)
|
||||
continue;
|
||||
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
|
||||
if (!denyMatch)
|
||||
continue;
|
||||
const parsed = parseInt(denyMatch[1], 10);
|
||||
if (!isNaN(parsed))
|
||||
return parsed;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function refreshPasswordAttemptPolicyHint() {
|
||||
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
|
||||
let lockoutConfigured = false;
|
||||
let denyFromPam = -1;
|
||||
for (let i = 0; i < pamSources.length; i++) {
|
||||
const source = pamSources[i];
|
||||
if (!source)
|
||||
continue;
|
||||
if (usesPamLockoutPolicy(source))
|
||||
lockoutConfigured = true;
|
||||
const denyValue = parsePamLineDenyValue(source);
|
||||
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
|
||||
denyFromPam = denyValue;
|
||||
}
|
||||
|
||||
if (!lockoutConfigured) {
|
||||
passwordAttemptLimitHint = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
|
||||
if (denyFromConfig >= 0) {
|
||||
passwordAttemptLimitHint = denyFromConfig;
|
||||
return;
|
||||
}
|
||||
|
||||
if (denyFromPam >= 0) {
|
||||
passwordAttemptLimitHint = denyFromPam;
|
||||
return;
|
||||
}
|
||||
|
||||
// pam_faillock default deny value when no explicit config is set.
|
||||
passwordAttemptLimitHint = 3;
|
||||
}
|
||||
|
||||
function isLikelyLockoutMessage(message) {
|
||||
const lower = (message || "").toLowerCase();
|
||||
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
|
||||
}
|
||||
|
||||
function currentAuthMessage() {
|
||||
if (GreeterState.pamState === "error")
|
||||
return "Authentication error - try again";
|
||||
if (GreeterState.pamState === "max")
|
||||
return "Too many failed attempts - account may be locked";
|
||||
if (GreeterState.pamState === "fail") {
|
||||
if (passwordAttemptLimitHint > 0) {
|
||||
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
|
||||
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
|
||||
if (remaining > 0) {
|
||||
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
|
||||
}
|
||||
return "Incorrect password - next failures may trigger account lockout";
|
||||
}
|
||||
return "Incorrect password";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function clearAuthFeedback() {
|
||||
GreeterState.pamState = "";
|
||||
authFeedbackMessage = "";
|
||||
}
|
||||
|
||||
function resetPasswordSessionTransition(clearSubmitRequest) {
|
||||
cancelingExternalAuthForPassword = false;
|
||||
passwordSessionTransitionRetryCount = 0;
|
||||
if (clearSubmitRequest)
|
||||
passwordSubmitRequested = false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GreetdSettings
|
||||
function onSettingsLoadedChanged() {
|
||||
if (GreetdSettings.settingsLoaded)
|
||||
if (GreetdSettings.settingsLoaded) {
|
||||
initWeatherService();
|
||||
if (isPrimaryScreen) {
|
||||
applyLastSuccessfulUser();
|
||||
finalizeSessionSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onRememberLastUserChanged() {
|
||||
if (!isPrimaryScreen)
|
||||
return;
|
||||
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
|
||||
GreetdMemory.setLastSuccessfulUser("");
|
||||
}
|
||||
applyLastSuccessfulUser();
|
||||
}
|
||||
|
||||
function onRememberLastSessionChanged() {
|
||||
if (!isPrimaryScreen)
|
||||
return;
|
||||
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
|
||||
GreetdMemory.setLastSessionId("");
|
||||
}
|
||||
finalizeSessionSelection();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: greetdPamWatcher
|
||||
path: "/etc/pam.d/greetd"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.greetdPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.greetdPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemAuthPamWatcher
|
||||
path: "/etc/pam.d/system-auth"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.systemAuthPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.systemAuthPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: commonAuthPamWatcher
|
||||
path: "/etc/pam.d/common-auth"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.commonAuthPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.commonAuthPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: passwordAuthPamWatcher
|
||||
path: "/etc/pam.d/password-auth"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.passwordAuthPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.passwordAuthPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemLoginPamWatcher
|
||||
path: "/etc/pam.d/system-login"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.systemLoginPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.systemLoginPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: systemLocalLoginPamWatcher
|
||||
path: "/etc/pam.d/system-local-login"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.systemLocalLoginPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.systemLocalLoginPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: commonAuthPcPamWatcher
|
||||
path: "/etc/pam.d/common-auth-pc"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.commonAuthPcPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.commonAuthPcPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: loginPamWatcher
|
||||
path: "/etc/pam.d/login"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.loginPamText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
root.maybeAutoStartExternalAuth();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.loginPamText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: faillockConfigWatcher
|
||||
path: "/etc/security/faillock.conf"
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
root.faillockConfigText = text();
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
onLoadFailed: {
|
||||
root.faillockConfigText = "";
|
||||
root.refreshPasswordAttemptPolicyHint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
initWeatherService();
|
||||
refreshPasswordAttemptPolicyHint();
|
||||
|
||||
if (isPrimaryScreen)
|
||||
applyLastSuccessfulUser();
|
||||
@@ -63,15 +433,131 @@ Item {
|
||||
}
|
||||
|
||||
function applyLastSuccessfulUser() {
|
||||
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||
return;
|
||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||
GreeterState.username = lastUser;
|
||||
GreeterState.usernameInput = lastUser;
|
||||
GreeterState.showPasswordInput = true;
|
||||
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: {
|
||||
if (weatherInitialized)
|
||||
WeatherService.removeRef();
|
||||
@@ -143,10 +629,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 {
|
||||
anchors.fill: parent
|
||||
screenName: root.screenName
|
||||
visible: {
|
||||
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||
return false;
|
||||
var _ = SessionData.perMonitorWallpaper;
|
||||
var __ = SessionData.monitorWallpapers;
|
||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||
@@ -159,12 +674,14 @@ Item {
|
||||
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
|
||||
var _ = SessionData.perMonitorWallpaper;
|
||||
var __ = SessionData.monitorWallpapers;
|
||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
||||
}
|
||||
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
|
||||
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
|
||||
smooth: true
|
||||
asynchronous: false
|
||||
cache: true
|
||||
@@ -327,10 +844,7 @@ Item {
|
||||
anchors.top: clockContainer.bottom
|
||||
anchors.topMargin: 4
|
||||
text: {
|
||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
|
||||
}
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
@@ -399,6 +913,9 @@ Item {
|
||||
if (GreeterState.showPasswordInput && revealButton.visible) {
|
||||
margin += revealButton.width;
|
||||
}
|
||||
if (externalAuthButton.visible) {
|
||||
margin += externalAuthButton.width;
|
||||
}
|
||||
if (virtualKeyboardButton.visible) {
|
||||
margin += virtualKeyboardButton.width;
|
||||
}
|
||||
@@ -415,21 +932,18 @@ Item {
|
||||
return;
|
||||
if (GreeterState.showPasswordInput) {
|
||||
GreeterState.passwordBuffer = text;
|
||||
if (!text || text.length === 0)
|
||||
root.passwordSubmitRequested = false;
|
||||
} else {
|
||||
GreeterState.usernameInput = text;
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (GreeterState.showPasswordInput) {
|
||||
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
|
||||
Greetd.createSession(GreeterState.username);
|
||||
}
|
||||
root.startAuthSession(true);
|
||||
} else {
|
||||
if (text.trim()) {
|
||||
GreeterState.username = text.trim();
|
||||
GreeterState.showPasswordInput = true;
|
||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
||||
GreeterState.passwordBuffer = "";
|
||||
root.submitUsername(text);
|
||||
syncingFromState = true;
|
||||
text = "";
|
||||
syncingFromState = false;
|
||||
@@ -461,14 +975,14 @@ Item {
|
||||
|
||||
anchors.left: lockIcon.right
|
||||
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.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (GreeterState.unlocking) {
|
||||
return "Logging in...";
|
||||
}
|
||||
if (Greetd.state !== GreetdState.Inactive) {
|
||||
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
|
||||
return "Authenticating...";
|
||||
}
|
||||
if (GreeterState.showPasswordInput) {
|
||||
@@ -476,7 +990,7 @@ Item {
|
||||
}
|
||||
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
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||
|
||||
@@ -498,7 +1012,7 @@ Item {
|
||||
StyledText {
|
||||
anchors.left: lockIcon.right
|
||||
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.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
@@ -528,15 +1042,27 @@ Item {
|
||||
DankActionButton {
|
||||
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.verticalCenter: parent.verticalCenter
|
||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||
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
|
||||
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 {
|
||||
id: virtualKeyboardButton
|
||||
|
||||
@@ -545,7 +1071,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard"
|
||||
buttonSize: 32
|
||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
if (keyboard_controller.isKeyboardActive) {
|
||||
@@ -564,19 +1090,14 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard_return"
|
||||
buttonSize: 36
|
||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||
enabled: true
|
||||
onClicked: {
|
||||
if (GreeterState.showPasswordInput) {
|
||||
if (GreeterState.username) {
|
||||
Greetd.createSession(GreeterState.username);
|
||||
}
|
||||
root.startAuthSession(true);
|
||||
} else {
|
||||
if (inputField.text.trim()) {
|
||||
GreeterState.username = inputField.text.trim();
|
||||
GreeterState.showPasswordInput = true;
|
||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
||||
GreeterState.passwordBuffer = "";
|
||||
root.submitUsername(inputField.text);
|
||||
inputField.text = "";
|
||||
}
|
||||
}
|
||||
@@ -601,20 +1122,16 @@ Item {
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 20
|
||||
Layout.preferredHeight: 38
|
||||
Layout.topMargin: -Theme.spacingS
|
||||
Layout.bottomMargin: -Theme.spacingS
|
||||
text: {
|
||||
if (GreeterState.pamState === "error")
|
||||
return "Authentication error - try again";
|
||||
if (GreeterState.pamState === "fail")
|
||||
return "Incorrect password";
|
||||
return "";
|
||||
}
|
||||
text: root.authFeedbackMessage
|
||||
color: Theme.error
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
opacity: GreeterState.pamState !== "" ? 1 : 0
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
opacity: root.authFeedbackMessage !== "" ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
@@ -667,6 +1184,7 @@ Item {
|
||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||
onClicked: {
|
||||
GreeterState.reset();
|
||||
root.externalAuthAutoStartedForUser = "";
|
||||
inputField.text = "";
|
||||
PortalService.profileImage = "";
|
||||
}
|
||||
@@ -1029,9 +1547,11 @@ Item {
|
||||
return;
|
||||
if (!GreetdMemory.memoryReady)
|
||||
return;
|
||||
if (!GreetdSettings.settingsLoaded)
|
||||
return;
|
||||
|
||||
const savedSession = GreetdMemory.lastSessionId;
|
||||
if (savedSession) {
|
||||
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
|
||||
if (savedSession && GreetdSettings.rememberLastSession) {
|
||||
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
||||
if (GreeterState.sessionPaths[i] === savedSession) {
|
||||
GreeterState.currentSessionIndex = i;
|
||||
@@ -1164,44 +1684,151 @@ Item {
|
||||
|
||||
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
||||
if (responseRequired) {
|
||||
Greetd.respond(GreeterState.passwordBuffer);
|
||||
GreeterState.passwordBuffer = "";
|
||||
inputField.text = "";
|
||||
cancelingExternalAuthForPassword = false;
|
||||
passwordSessionTransitionRetryCount = 0;
|
||||
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;
|
||||
}
|
||||
if (!error)
|
||||
Greetd.respond("");
|
||||
pendingPasswordResponse = false;
|
||||
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() {
|
||||
awaitingExternalAuth = false;
|
||||
pendingPasswordResponse = false;
|
||||
resetPasswordSessionTransition(true);
|
||||
authTimeout.interval = defaultAuthTimeoutMs;
|
||||
authTimeout.stop();
|
||||
passwordFailureCount = 0;
|
||||
clearAuthFeedback();
|
||||
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
||||
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
||||
if (!sessionCmd) {
|
||||
GreeterState.pamState = "error";
|
||||
authFeedbackMessage = currentAuthMessage();
|
||||
placeholderDelay.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
GreeterState.unlocking = true;
|
||||
launchTimeout.restart();
|
||||
GreetdMemory.setLastSessionId(sessionPath);
|
||||
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
||||
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
||||
if (GreetdSettings.rememberLastSession) {
|
||||
GreetdMemory.setLastSessionId(sessionPath);
|
||||
} 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) {
|
||||
awaitingExternalAuth = false;
|
||||
pendingPasswordResponse = false;
|
||||
resetPasswordSessionTransition(true);
|
||||
authTimeout.interval = defaultAuthTimeoutMs;
|
||||
authTimeout.stop();
|
||||
launchTimeout.stop();
|
||||
GreeterState.unlocking = false;
|
||||
GreeterState.pamState = "fail";
|
||||
if (isLikelyLockoutMessage(message)) {
|
||||
GreeterState.pamState = "max";
|
||||
} else {
|
||||
GreeterState.pamState = "fail";
|
||||
passwordFailureCount = passwordFailureCount + 1;
|
||||
}
|
||||
authFeedbackMessage = currentAuthMessage();
|
||||
GreeterState.passwordBuffer = "";
|
||||
inputField.text = "";
|
||||
placeholderDelay.restart();
|
||||
Greetd.cancelSession();
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
awaitingExternalAuth = false;
|
||||
pendingPasswordResponse = false;
|
||||
resetPasswordSessionTransition(true);
|
||||
authTimeout.interval = defaultAuthTimeoutMs;
|
||||
authTimeout.stop();
|
||||
launchTimeout.stop();
|
||||
GreeterState.unlocking = false;
|
||||
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 = "";
|
||||
inputField.text = "";
|
||||
placeholderDelay.restart();
|
||||
@@ -1215,8 +1842,11 @@ Item {
|
||||
onTriggered: {
|
||||
if (!GreeterState.unlocking)
|
||||
return;
|
||||
pendingPasswordResponse = false;
|
||||
resetPasswordSessionTransition(true);
|
||||
GreeterState.unlocking = false;
|
||||
GreeterState.pamState = "error";
|
||||
authFeedbackMessage = currentAuthMessage();
|
||||
placeholderDelay.restart();
|
||||
Greetd.cancelSession();
|
||||
}
|
||||
@@ -1225,7 +1855,7 @@ Item {
|
||||
Timer {
|
||||
id: placeholderDelay
|
||||
interval: 4000
|
||||
onTriggered: GreeterState.pamState = ""
|
||||
onTriggered: clearAuthFeedback()
|
||||
}
|
||||
|
||||
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.
|
||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
||||
- **Session Memory**: Remembers last selected session and user
|
||||
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
|
||||
dms-greeter --command sway
|
||||
dms-greeter --command mangowc
|
||||
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`:
|
||||
|
||||
@@ -6,6 +6,9 @@ COMPOSITOR=""
|
||||
COMPOSITOR_CONFIG=""
|
||||
DMS_PATH="dms-greeter"
|
||||
CACHE_DIR="/var/cache/dms-greeter"
|
||||
REMEMBER_LAST_SESSION=""
|
||||
REMEMBER_LAST_USER=""
|
||||
DEBUG_MODE=0
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
@@ -22,6 +25,15 @@ Options:
|
||||
(default: dms-greeter)
|
||||
--cache-dir PATH Cache directory for greeter data
|
||||
(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
|
||||
|
||||
Examples:
|
||||
@@ -30,6 +42,7 @@ Examples:
|
||||
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 niri --cache-dir /tmp/dmsgreeter
|
||||
dms-greeter --command niri --no-save-session --no-save-username
|
||||
dms-greeter --command mango
|
||||
dms-greeter --command labwc
|
||||
EOF
|
||||
@@ -43,6 +56,41 @@ require_command() {
|
||||
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
|
||||
case $1 in
|
||||
--command)
|
||||
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
|
||||
CACHE_DIR="$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)
|
||||
show_help
|
||||
exit 0
|
||||
@@ -111,9 +179,58 @@ export QT_QPA_PLATFORM=wayland
|
||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||
export EGL_PLATFORM=gbm
|
||||
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"
|
||||
|
||||
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
|
||||
QS_BIN="qs"
|
||||
@@ -130,7 +247,9 @@ if [[ "$DMS_PATH" == /* ]]; then
|
||||
else
|
||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
||||
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"
|
||||
else
|
||||
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"
|
||||
NIRI_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
exec niri -c "$COMPOSITOR_CONFIG"
|
||||
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
||||
;;
|
||||
|
||||
hyprland)
|
||||
@@ -222,9 +341,9 @@ HYPRLAND_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
fi
|
||||
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
|
||||
exec Hyprland -c "$COMPOSITOR_CONFIG"
|
||||
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -245,7 +364,7 @@ exec "$QS_CMD; swaymsg exit"
|
||||
SWAY_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
fi
|
||||
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
||||
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
||||
;;
|
||||
|
||||
scroll)
|
||||
@@ -265,7 +384,7 @@ exec "$QS_CMD; scrollmsg exit"
|
||||
SCROLL_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
fi
|
||||
exec scroll -c "$COMPOSITOR_CONFIG"
|
||||
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
||||
;;
|
||||
|
||||
miracle|miracle-wm)
|
||||
@@ -285,24 +404,24 @@ exec "$QS_CMD; miraclemsg exit"
|
||||
MIRACLE_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
fi
|
||||
exec miracle-wm -c "$COMPOSITOR_CONFIG"
|
||||
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
||||
;;
|
||||
|
||||
labwc)
|
||||
require_command "labwc"
|
||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
||||
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
||||
else
|
||||
exec labwc --session "$QS_CMD"
|
||||
exec_compositor "labwc" labwc --session "$QS_CMD"
|
||||
fi
|
||||
;;
|
||||
|
||||
mango|mangowc)
|
||||
require_command "mango"
|
||||
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
|
||||
exec mango -s "$QS_CMD && mmsg -d quit"
|
||||
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
@@ -755,7 +755,7 @@ Item {
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
||||
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
|
||||
pam.passwd.start();
|
||||
}
|
||||
}
|
||||
@@ -764,6 +764,11 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.unlocking) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (pam.u2fPending) {
|
||||
pam.cancelU2fPending();
|
||||
@@ -1017,7 +1022,7 @@ Item {
|
||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||
enabled: !demoMode
|
||||
onClicked: {
|
||||
if (!demoMode && !pam.u2fPending) {
|
||||
if (!demoMode && !root.unlocking && !pam.u2fPending) {
|
||||
pam.passwd.start();
|
||||
}
|
||||
}
|
||||
@@ -1626,6 +1631,7 @@ Item {
|
||||
onStateChanged: {
|
||||
root.pamState = state;
|
||||
if (state !== "") {
|
||||
root.unlocking = false;
|
||||
placeholderDelay.restart();
|
||||
passwordField.text = "";
|
||||
root.passwordBuffer = "";
|
||||
@@ -1641,6 +1647,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pam
|
||||
|
||||
function onUnlockInProgressChanged() {
|
||||
if (!pam.unlockInProgress && root.unlocking)
|
||||
root.unlocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: pam
|
||||
property: "buffer"
|
||||
|
||||
@@ -25,6 +25,29 @@ Scope {
|
||||
signal flashMsg
|
||||
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 {
|
||||
if (!unlockInProgress) {
|
||||
unlockInProgress = true;
|
||||
@@ -36,6 +59,7 @@ Scope {
|
||||
u2fPendingTimeout.running = false;
|
||||
u2fPending = false;
|
||||
u2fState = "";
|
||||
unlockRequestTimeout.restart();
|
||||
unlockRequested();
|
||||
}
|
||||
}
|
||||
@@ -66,6 +90,13 @@ Scope {
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: loginConfigWatcher
|
||||
|
||||
path: "/etc/pam.d/login"
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: u2fConfigWatcher
|
||||
|
||||
@@ -77,7 +108,7 @@ Scope {
|
||||
id: passwd
|
||||
|
||||
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: {
|
||||
if (message.startsWith("The account is locked"))
|
||||
@@ -102,6 +133,13 @@ Scope {
|
||||
return;
|
||||
}
|
||||
|
||||
unlockRequestTimeout.running = false;
|
||||
root.unlockInProgress = false;
|
||||
root.u2fPending = false;
|
||||
root.u2fState = "";
|
||||
u2fPendingTimeout.running = false;
|
||||
u2f.abort();
|
||||
|
||||
if (res === PamResult.Error)
|
||||
root.state = "error";
|
||||
else if (res === PamResult.MaxTries)
|
||||
@@ -114,10 +152,22 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: passwd
|
||||
|
||||
function onActiveChanged() {
|
||||
if (passwd.active) {
|
||||
passwdActiveTimeout.restart();
|
||||
} else {
|
||||
passwdActiveTimeout.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: fprint
|
||||
|
||||
property bool available
|
||||
property bool available: SettingsData.lockFingerprintReady
|
||||
property int tries
|
||||
property int errorTries
|
||||
|
||||
@@ -173,7 +223,7 @@ Scope {
|
||||
PamContext {
|
||||
id: u2f
|
||||
|
||||
property bool available
|
||||
property bool available: SettingsData.lockU2fReady
|
||||
|
||||
function checkAvail(): void {
|
||||
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
|
||||
@@ -238,26 +288,6 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: availProc
|
||||
|
||||
command: ["sh", "-c", "fprintd-list $USER"]
|
||||
onExited: code => {
|
||||
fprint.available = code === 0;
|
||||
fprint.checkAvail();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: u2fAvailProc
|
||||
|
||||
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
||||
onExited: code => {
|
||||
u2f.available = code === 0;
|
||||
u2f.checkAvail();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: errorRetry
|
||||
|
||||
@@ -279,6 +309,26 @@ Scope {
|
||||
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 {
|
||||
id: stateReset
|
||||
|
||||
@@ -301,24 +351,17 @@ Scope {
|
||||
|
||||
onLockSecuredChanged: {
|
||||
if (lockSecured) {
|
||||
availProc.running = true;
|
||||
u2fAvailProc.running = true;
|
||||
SettingsData.refreshAuthAvailability();
|
||||
root.state = "";
|
||||
root.fprintState = "";
|
||||
root.u2fState = "";
|
||||
root.u2fPending = false;
|
||||
root.lockMessage = "";
|
||||
root.unlockInProgress = false;
|
||||
root.resetAuthFlows();
|
||||
fprint.checkAvail();
|
||||
u2f.checkAvail();
|
||||
} else {
|
||||
fprint.abort();
|
||||
passwd.abort();
|
||||
u2f.abort();
|
||||
errorRetry.running = false;
|
||||
u2fErrorRetry.running = false;
|
||||
u2fPendingTimeout.running = false;
|
||||
root.u2fPending = false;
|
||||
root.u2fState = "";
|
||||
root.unlockInProgress = false;
|
||||
root.resetAuthFlows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,15 +372,24 @@ Scope {
|
||||
fprint.checkAvail();
|
||||
}
|
||||
|
||||
function onLockFingerprintReadyChanged(): void {
|
||||
fprint.checkAvail();
|
||||
}
|
||||
|
||||
function onEnableU2fChanged(): void {
|
||||
u2f.checkAvail();
|
||||
}
|
||||
|
||||
function onLockU2fReadyChanged(): void {
|
||||
u2f.checkAvail();
|
||||
}
|
||||
|
||||
function onU2fModeChanged(): void {
|
||||
if (root.lockSecured) {
|
||||
u2f.abort();
|
||||
u2fErrorRetry.running = false;
|
||||
u2fPendingTimeout.running = false;
|
||||
unlockRequestTimeout.running = false;
|
||||
root.u2fPending = false;
|
||||
root.u2fState = "";
|
||||
u2f.checkAvail();
|
||||
|
||||
@@ -7,7 +7,7 @@ DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:notification-center-popout"
|
||||
fullHeightSurface: false
|
||||
fullHeightSurface: true
|
||||
|
||||
property bool notificationHistoryVisible: false
|
||||
property var triggerScreen: null
|
||||
|
||||
@@ -108,6 +108,13 @@ QtObject {
|
||||
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) {
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (!_isValidWindow(p) || p.exiting)
|
||||
@@ -118,7 +125,7 @@ QtObject {
|
||||
}
|
||||
}
|
||||
for (const w of newWrappers) {
|
||||
if (w && !_hasWindowFor(w))
|
||||
if (w && !_hasWindowFor(w) && _isFocusedScreen())
|
||||
_insertAtTop(w);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,6 +909,9 @@ Singleton {
|
||||
case "dwl":
|
||||
DwlService.generateOutputsConfig(outputsData);
|
||||
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 {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show on Last Display")
|
||||
|
||||
@@ -160,6 +160,16 @@ Item {
|
||||
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 {
|
||||
settingKey: "dockIndicatorStyle"
|
||||
tags: ["dock", "indicator", "style", "circle", "line"]
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
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.greeterFingerprintCanEnable || SettingsData.greeterEnableFprint
|
||||
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
|
||||
|
||||
function greeterFingerprintDescription() {
|
||||
const source = SettingsData.greeterFingerprintSource;
|
||||
const reason = SettingsData.greeterFingerprintReason;
|
||||
|
||||
if (source === "pam") {
|
||||
switch (reason) {
|
||||
case "configured_externally":
|
||||
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.");
|
||||
case "missing_enrollment":
|
||||
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet.");
|
||||
case "missing_reader":
|
||||
return I18n.tr("PAM provides fingerprint auth, but no reader was detected.");
|
||||
default:
|
||||
return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
switch (reason) {
|
||||
case "ready":
|
||||
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.");
|
||||
case "missing_enrollment":
|
||||
if (SettingsData.greeterEnableFprint)
|
||||
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.");
|
||||
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.");
|
||||
case "missing_reader":
|
||||
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
|
||||
case "missing_pam_support":
|
||||
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.");
|
||||
default:
|
||||
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
function greeterU2fDescription() {
|
||||
const source = SettingsData.greeterU2fSource;
|
||||
const reason = SettingsData.greeterU2fReason;
|
||||
|
||||
if (source === "pam") {
|
||||
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login.");
|
||||
}
|
||||
|
||||
switch (reason) {
|
||||
case "ready":
|
||||
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Available.");
|
||||
case "missing_key_registration":
|
||||
if (SettingsData.greeterEnableU2f)
|
||||
return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync.");
|
||||
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
|
||||
case "missing_pam_support":
|
||||
return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.");
|
||||
default:
|
||||
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
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: root.greeterFingerprintDescription()
|
||||
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? 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: root.greeterU2fDescription()
|
||||
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? 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,51 @@ import qs.Modules.Settings.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
|
||||
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
|
||||
|
||||
function lockFingerprintDescription() {
|
||||
switch (SettingsData.lockFingerprintReason) {
|
||||
case "ready":
|
||||
return I18n.tr("Use fingerprint authentication for the lock screen.");
|
||||
case "missing_enrollment":
|
||||
if (SettingsData.enableFprint)
|
||||
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.");
|
||||
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
|
||||
case "missing_reader":
|
||||
return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
|
||||
case "missing_pam_support":
|
||||
return I18n.tr("Not available — install fprintd and pam_fprintd.");
|
||||
default:
|
||||
return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
function lockU2fDescription() {
|
||||
switch (SettingsData.lockU2fReason) {
|
||||
case "ready":
|
||||
return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
|
||||
case "missing_key_registration":
|
||||
if (SettingsData.enableU2f)
|
||||
return I18n.tr("Enabled, but no registered security key was found yet. Register a key or update your U2F config.");
|
||||
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
|
||||
case "missing_pam_support":
|
||||
return I18n.tr("Not available — install or configure pam_u2f.");
|
||||
default:
|
||||
return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAuthDetection() {
|
||||
SettingsData.refreshAuthAvailability();
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshAuthDetection()
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
refreshAuthDetection();
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: videoBrowserModal
|
||||
browserTitle: I18n.tr("Select Video or Folder")
|
||||
@@ -172,10 +217,10 @@ Item {
|
||||
settingKey: "enableFprint"
|
||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||
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")
|
||||
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||
description: root.lockFingerprintDescription()
|
||||
descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||
checked: SettingsData.enableFprint
|
||||
enabled: SettingsData.fprintdAvailable
|
||||
enabled: root.lockFprintToggleAvailable
|
||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||
}
|
||||
|
||||
@@ -183,10 +228,10 @@ Item {
|
||||
settingKey: "enableU2f"
|
||||
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")
|
||||
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")
|
||||
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||
description: root.lockU2fDescription()
|
||||
descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||
checked: SettingsData.enableU2f
|
||||
enabled: SettingsData.u2fAvailable
|
||||
enabled: root.lockU2fToggleAvailable
|
||||
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||
}
|
||||
|
||||
@@ -195,7 +240,7 @@ Item {
|
||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||
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")
|
||||
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")]
|
||||
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 => {
|
||||
@@ -245,7 +290,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Path to a video file or folder containing videos")
|
||||
font.pixelSize: Theme.fontSizeXSmall
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outlineVariant
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
|
||||
@@ -1935,11 +1935,6 @@ Item {
|
||||
label: I18n.tr("Auth Type"),
|
||||
value: data["connection-type"]
|
||||
});
|
||||
fields.push({
|
||||
label: I18n.tr("Autoconnect"),
|
||||
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
|
||||
});
|
||||
|
||||
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 {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
|
||||
@@ -20,7 +20,10 @@ Item {
|
||||
var out = [];
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
if ((rules[i].action || "").toString().toLowerCase() === "mute")
|
||||
out.push({ rule: rules[i], index: i });
|
||||
out.push({
|
||||
rule: rules[i],
|
||||
index: i
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -288,6 +291,15 @@ Item {
|
||||
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 {
|
||||
width: parent.width
|
||||
height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2
|
||||
@@ -340,6 +352,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: animationDurationSlider
|
||||
settingKey: "notificationCustomAnimationDuration"
|
||||
tags: ["notification", "animation", "duration", "custom", "speed"]
|
||||
text: I18n.tr("Duration")
|
||||
@@ -355,6 +368,13 @@ Item {
|
||||
}
|
||||
SettingsData.set("notificationCustomAnimationDuration", newValue);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onNotificationAnimationBaseDurationChanged() {
|
||||
animationDurationSlider.value = Theme.notificationAnimationBaseDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,6 +408,8 @@ FloatingWindow {
|
||||
}
|
||||
clip: true
|
||||
visible: !root.isLoading
|
||||
add: null
|
||||
displaced: null
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: browserScrollbar
|
||||
|
||||
@@ -2638,6 +2638,18 @@ Item {
|
||||
checked: SettingsData.matugenTemplateEmacs
|
||||
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 {
|
||||
|
||||
@@ -125,6 +125,16 @@ Item {
|
||||
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 {
|
||||
settingKey: "workspaceFollowFocus"
|
||||
tags: ["workspace", "focus", "follow", "monitor"]
|
||||
|
||||
@@ -27,7 +27,9 @@ quickshell -p quickshell/
|
||||
**Code formatting:**
|
||||
```bash
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
@@ -93,9 +93,9 @@ Singleton {
|
||||
`;
|
||||
|
||||
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.timeout = Qt.binding(() => root.monitorTimeout);
|
||||
monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0);
|
||||
monitorOffMonitor.isIdleChanged.connect(function () {
|
||||
if (monitorOffMonitor.isIdle) {
|
||||
if (SettingsData.fadeToDpmsEnabled) {
|
||||
@@ -112,9 +112,9 @@ Singleton {
|
||||
});
|
||||
|
||||
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.timeout = Qt.binding(() => root.lockTimeout);
|
||||
lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0);
|
||||
lockMonitor.isIdleChanged.connect(function () {
|
||||
if (lockMonitor.isIdle) {
|
||||
if (SettingsData.fadeToLockEnabled) {
|
||||
@@ -130,9 +130,9 @@ Singleton {
|
||||
});
|
||||
|
||||
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.timeout = Qt.binding(() => root.suspendTimeout);
|
||||
suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0);
|
||||
suspendMonitor.isIdleChanged.connect(function () {
|
||||
if (suspendMonitor.isIdle) {
|
||||
root.requestSuspend();
|
||||
|
||||
@@ -142,6 +142,7 @@ Singleton {
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("VPN configuration updated"));
|
||||
DMSNetworkService.refreshVpnProfiles();
|
||||
getConfig(uuid);
|
||||
configUpdated();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ Singleton {
|
||||
}
|
||||
|
||||
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.currentWallpaper = currentWallpaper;
|
||||
process.goToPrevious = false;
|
||||
@@ -272,7 +272,7 @@ Singleton {
|
||||
}
|
||||
} else {
|
||||
// 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.currentWallpaper = currentWallpaper;
|
||||
cyclingProcess.running = true;
|
||||
@@ -296,7 +296,7 @@ Singleton {
|
||||
}
|
||||
|
||||
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.currentWallpaper = currentWallpaper;
|
||||
process.goToPrevious = true;
|
||||
@@ -304,7 +304,7 @@ Singleton {
|
||||
}
|
||||
} else {
|
||||
// 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.currentWallpaper = currentWallpaper;
|
||||
prevCyclingProcess.running = true;
|
||||
|
||||
@@ -18,258 +18,329 @@ Singleton {
|
||||
target: DMSService
|
||||
|
||||
function onCapabilitiesReceived() {
|
||||
checkCapabilities()
|
||||
checkCapabilities();
|
||||
}
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkCapabilities()
|
||||
return
|
||||
checkCapabilities();
|
||||
return;
|
||||
}
|
||||
wlrOutputAvailable = false
|
||||
wlrOutputAvailable = false;
|
||||
}
|
||||
|
||||
function onWlrOutputStateUpdate(data) {
|
||||
if (!wlrOutputAvailable) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
handleStateUpdate(data)
|
||||
handleStateUpdate(data);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!DMSService.dmsAvailable) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
checkCapabilities()
|
||||
checkCapabilities();
|
||||
}
|
||||
|
||||
function checkCapabilities() {
|
||||
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
||||
wlrOutputAvailable = false
|
||||
return
|
||||
wlrOutputAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const hasWlrOutput = DMSService.capabilities.includes("wlroutput")
|
||||
const hasWlrOutput = DMSService.capabilities.includes("wlroutput");
|
||||
if (hasWlrOutput && !wlrOutputAvailable) {
|
||||
wlrOutputAvailable = true
|
||||
console.info("WlrOutputService: wlr-output-management capability detected")
|
||||
requestState()
|
||||
return
|
||||
wlrOutputAvailable = true;
|
||||
console.info("WlrOutputService: wlr-output-management capability detected");
|
||||
requestState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasWlrOutput) {
|
||||
wlrOutputAvailable = false
|
||||
wlrOutputAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
function requestState() {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wlroutput.getState", null, response => {
|
||||
if (!response.result) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
handleStateUpdate(response.result)
|
||||
})
|
||||
handleStateUpdate(response.result);
|
||||
});
|
||||
}
|
||||
|
||||
function handleStateUpdate(state) {
|
||||
outputs = state.outputs || []
|
||||
serial = state.serial || 0
|
||||
outputs = state.outputs || [];
|
||||
serial = state.serial || 0;
|
||||
|
||||
if (outputs.length === 0) {
|
||||
console.warn("WlrOutputService: Received empty outputs list")
|
||||
console.warn("WlrOutputService: Received empty outputs list");
|
||||
} 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) => {
|
||||
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")
|
||||
})
|
||||
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");
|
||||
});
|
||||
}
|
||||
stateChanged()
|
||||
stateChanged();
|
||||
}
|
||||
|
||||
function getOutput(name) {
|
||||
for (const output of outputs) {
|
||||
if (output.name === name) {
|
||||
return output
|
||||
return output;
|
||||
}
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
function getEnabledOutputs() {
|
||||
return outputs.filter(output => output.enabled)
|
||||
return outputs.filter(output => output.enabled);
|
||||
}
|
||||
|
||||
function applyConfiguration(heads, callback) {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
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) => {
|
||||
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)
|
||||
})
|
||||
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);
|
||||
});
|
||||
|
||||
DMSService.sendRequest("wlroutput.applyConfiguration", {
|
||||
"heads": heads
|
||||
}, response => {
|
||||
const success = !response.error
|
||||
const message = response.error || response.result?.message || ""
|
||||
const success = !response.error;
|
||||
const message = response.error || response.result?.message || "";
|
||||
|
||||
if (response.error) {
|
||||
console.warn("WlrOutputService: applyConfiguration error:", response.error)
|
||||
console.warn("WlrOutputService: applyConfiguration error:", response.error);
|
||||
} else {
|
||||
console.log("WlrOutputService: Configuration applied successfully")
|
||||
console.log("WlrOutputService: Configuration applied successfully");
|
||||
}
|
||||
|
||||
configurationApplied(success, message)
|
||||
configurationApplied(success, message);
|
||||
if (callback) {
|
||||
callback(success, message)
|
||||
callback(success, message);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function testConfiguration(heads, callback) {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
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", {
|
||||
"heads": heads
|
||||
}, response => {
|
||||
const success = !response.error
|
||||
const message = response.error || response.result?.message || ""
|
||||
const success = !response.error;
|
||||
const message = response.error || response.result?.message || "";
|
||||
|
||||
if (response.error) {
|
||||
console.warn("WlrOutputService: testConfiguration error:", response.error)
|
||||
console.warn("WlrOutputService: testConfiguration error:", response.error);
|
||||
} else {
|
||||
console.log("WlrOutputService: Configuration test passed")
|
||||
console.log("WlrOutputService: Configuration test passed");
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(success, message)
|
||||
callback(success, message);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function setOutputEnabled(outputName, enabled, callback) {
|
||||
const output = getOutput(outputName)
|
||||
const output = getOutput(outputName);
|
||||
if (!output) {
|
||||
console.warn("WlrOutputService: Output not found:", outputName)
|
||||
console.warn("WlrOutputService: Output not found:", outputName);
|
||||
if (callback) {
|
||||
callback(false, "Output not found")
|
||||
callback(false, "Output not found");
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": enabled
|
||||
}]
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": enabled
|
||||
}
|
||||
];
|
||||
|
||||
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) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"modeId": modeId
|
||||
}]
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"modeId": modeId
|
||||
}
|
||||
];
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
applyConfiguration(heads, callback);
|
||||
}
|
||||
|
||||
function setOutputCustomMode(outputName, width, height, refresh, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"customMode": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"refresh": refresh
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"customMode": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"refresh": refresh
|
||||
}
|
||||
}
|
||||
}]
|
||||
];
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
applyConfiguration(heads, callback);
|
||||
}
|
||||
|
||||
function setOutputPosition(outputName, x, y, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"position": {
|
||||
"x": x,
|
||||
"y": y
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"position": {
|
||||
"x": x,
|
||||
"y": y
|
||||
}
|
||||
}
|
||||
}]
|
||||
];
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
applyConfiguration(heads, callback);
|
||||
}
|
||||
|
||||
function setOutputScale(outputName, scale, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"scale": scale
|
||||
}]
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"scale": scale
|
||||
}
|
||||
];
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
applyConfiguration(heads, callback);
|
||||
}
|
||||
|
||||
function setOutputTransform(outputName, transform, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"transform": transform
|
||||
}]
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"transform": transform
|
||||
}
|
||||
];
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
applyConfiguration(heads, callback);
|
||||
}
|
||||
|
||||
function setOutputAdaptiveSync(outputName, state, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"adaptiveSync": state
|
||||
}]
|
||||
const heads = [
|
||||
{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"adaptiveSync": state
|
||||
}
|
||||
];
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
applyConfiguration(heads, callback);
|
||||
}
|
||||
|
||||
function configureOutput(config, callback) {
|
||||
const heads = [config]
|
||||
applyConfiguration(heads, callback)
|
||||
const heads = [config];
|
||||
applyConfiguration(heads, 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();
|
||||
return;
|
||||
}
|
||||
|
||||
dropdownMenu.open();
|
||||
|
||||
let currentIndex = root.options.indexOf(root.currentValue);
|
||||
listView.positionViewAtIndex(currentIndex, ListView.Beginning);
|
||||
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
|
||||
const popupW = dropdownMenu.width;
|
||||
const popupH = dropdownMenu.height;
|
||||
|
||||
@@ -239,7 +239,7 @@ Item {
|
||||
StyledRect {
|
||||
id: valueTooltip
|
||||
|
||||
width: tooltipText.contentWidth + Theme.spacingS * 2
|
||||
width: tooltipText.reservedWidth + Theme.spacingS * 2
|
||||
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
@@ -251,10 +251,22 @@ Item {
|
||||
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
StyledText {
|
||||
NumericText {
|
||||
id: tooltipText
|
||||
|
||||
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
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
|
||||
@@ -99,10 +99,12 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
color: (checked && enabled) ? Theme.primary : Theme.surfaceVariantAlpha
|
||||
opacity: toggling ? 0.6 : (enabled ? 1 : 0.4)
|
||||
// M3 disabled track: on surface 12% opacity
|
||||
color: !toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
|
||||
opacity: toggle.toggling ? 0.6 : 1
|
||||
|
||||
border.color: (!checked || !enabled) ? Theme.outline : "transparent"
|
||||
// M3 disabled unchecked border: on surface 12% opacity
|
||||
border.color: toggle.checked ? "transparent" : (!toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.outline)
|
||||
|
||||
readonly property int pad: Math.round((height - thumb.width) / 2)
|
||||
readonly property int edgeLeft: pad
|
||||
@@ -111,16 +113,18 @@ Item {
|
||||
StyledRect {
|
||||
id: thumb
|
||||
|
||||
width: (checked && enabled) ? insetCircle : insetCircle - 4
|
||||
height: (checked && enabled) ? insetCircle : insetCircle - 4
|
||||
width: toggle.checked ? insetCircle : insetCircle - 4
|
||||
height: toggle.checked ? insetCircle : insetCircle - 4
|
||||
radius: Theme.cornerRadius
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: (checked && enabled) ? Theme.surface : Theme.outline
|
||||
border.color: (checked && enabled) ? Theme.outline : Theme.outline
|
||||
border.width: (checked && enabled) ? 1 : 2
|
||||
// M3 disabled thumb:
|
||||
// checked = solid surface | unchecked = on surface 38%
|
||||
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Qt.alpha(Theme.surfaceText, 0.38)) : (toggle.checked ? Theme.surface : Theme.outline)
|
||||
border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
|
||||
border.width: (toggle.checked && toggle.enabled) ? 1 : 2
|
||||
|
||||
x: (checked && enabled) ? toggleTrack.edgeRight : toggleTrack.edgeLeft
|
||||
x: toggle.checked ? toggleTrack.edgeRight : toggleTrack.edgeLeft
|
||||
|
||||
Behavior on x {
|
||||
SequentialAnimation {
|
||||
@@ -158,10 +162,11 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
// M3 disabled icon: on surface 38%
|
||||
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
|
||||
filled: true
|
||||
opacity: checked && enabled ? 1 : 0
|
||||
scale: checked && enabled ? 1 : 0.6
|
||||
opacity: toggle.checked ? 1 : 0
|
||||
scale: toggle.checked ? 1 : 0.6
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
|
||||
@@ -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"),
|
||||
"value": data["connection-type"]
|
||||
});
|
||||
fields.push({
|
||||
"key": "auto",
|
||||
"label": I18n.tr("Autoconnect"),
|
||||
"value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
|
||||
});
|
||||
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 {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#%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}}
|
||||
background={{colors.background.default.hex_stripped}}
|
||||
selection-foreground={{colors.on_surface.default.hex_stripped}}
|
||||
|
||||
@@ -26,7 +26,8 @@ LANGUAGES = {
|
||||
"hu": "hu.json",
|
||||
"fa": "fa.json",
|
||||
"fr": "fr.json",
|
||||
"nl": "nl.json"
|
||||
"nl": "nl.json",
|
||||
"ru": "ru.json"
|
||||
}
|
||||
|
||||
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user