mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-22 03:04:01 -04:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
686eb1bfd6 | ||
|
|
4a87e8227d | ||
|
|
c0b26caf2b | ||
|
|
c6c40dbced | ||
|
|
02ddd010a0 | ||
|
|
6eb9df87cf | ||
|
|
0adbdfdde1 | ||
|
|
fb002d1977 | ||
|
|
8b7c9085ca | ||
|
|
8b34bbf71a | ||
|
|
fd468a6b78 | ||
|
|
7c04b61bcc | ||
|
|
bc4916a132 | ||
|
|
58a356bb05 | ||
|
|
3061e1301d | ||
|
|
e333da2013 | ||
|
|
307d05d76c | ||
|
|
10dbe73335 | ||
|
|
b6fee3a98f | ||
|
|
6166d51c7c | ||
|
|
d06fcccff9 | ||
|
|
d9cfeded47 | ||
|
|
c4ced3ac9d | ||
|
|
ce2ec80d80 | ||
|
|
a340eba493 | ||
|
|
2db7646fe3 | ||
|
|
097de493bb | ||
|
|
a7db0354ae | ||
|
|
2d2502cc67 | ||
|
|
402a8515ee | ||
|
|
4cf0f0c898 | ||
|
|
fd0564a3ec | ||
|
|
07f8675a7b | ||
|
|
cab2a1a3bc | ||
|
|
41dd91fe75 | ||
|
|
c074b220ee | ||
|
|
243d716703 | ||
|
|
fde2181c3b | ||
|
|
a5ffd18596 | ||
|
|
e0aa1f0177 | ||
|
|
e73b9c3ce1 | ||
|
|
69955286a7 | ||
|
|
d3a02f6342 | ||
|
|
86234e5b09 | ||
|
|
938fabe27d | ||
|
|
1b34aa0ce1 | ||
|
|
76ed5d68fa | ||
|
|
b69496dbcd | ||
|
|
ef179f9c92 | ||
|
|
b451d4d8e0 | ||
|
|
12f7675950 | ||
|
|
06cd88e990 |
@@ -520,37 +520,37 @@ jobs:
|
||||
# Pin the satellite repos to this release's version. Each dispatch target
|
||||
# decides for itself whether to tag: X.Y.0 always tags, X.Y.Z skips when the
|
||||
# repo has no commits of its own since its last tag.
|
||||
fanout:
|
||||
name: Pin ${{ matrix.repo }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release]
|
||||
if: ${{ !contains(inputs.tag, '-') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
repo:
|
||||
- dgop
|
||||
- dankgo
|
||||
# fanout:
|
||||
# name: Pin ${{ matrix.repo }}
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [release]
|
||||
# if: ${{ !contains(inputs.tag, '-') }}
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# repo:
|
||||
# - dgop
|
||||
# - dankgo
|
||||
# - dank-qml-common
|
||||
- dankcalendar
|
||||
- danksearch
|
||||
- dank-greeter
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ matrix.repo }}
|
||||
|
||||
- name: Dispatch pin-release
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
REPO: ${{ github.repository_owner }}/${{ matrix.repo }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh workflow run pin-release.yml --repo "$REPO" -f version="$TAG"
|
||||
echo "dispatched pin-release ${TAG} to ${REPO}" >> "$GITHUB_STEP_SUMMARY"
|
||||
# - dankcalendar
|
||||
# - danksearch
|
||||
# - dank-greeter
|
||||
# steps:
|
||||
# - name: Create GitHub App token
|
||||
# id: app_token
|
||||
# uses: actions/create-github-app-token@v3
|
||||
# with:
|
||||
# app-id: ${{ secrets.APP_ID }}
|
||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
# owner: ${{ github.repository_owner }}
|
||||
# repositories: ${{ matrix.repo }}
|
||||
#
|
||||
# - name: Dispatch pin-release
|
||||
# env:
|
||||
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
# REPO: ${{ github.repository_owner }}/${{ matrix.repo }}
|
||||
# TAG: ${{ inputs.tag }}
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# gh workflow run pin-release.yml --repo "$REPO" -f version="$TAG"
|
||||
# echo "dispatched pin-release ${TAG} to ${REPO}" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[submodule "dank-qml-common"]
|
||||
path = dank-qml-common
|
||||
url = https://github.com/AvengeMedia/dank-qml-common.git
|
||||
branch = master
|
||||
branch = stable-1.6
|
||||
|
||||
@@ -79,11 +79,11 @@ func runDank16(cmd *cobra.Command, args []string) {
|
||||
primaryLight = primaryColor
|
||||
}
|
||||
variantOpts := dank16.VariantOptions{
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
Background: background,
|
||||
UseDPS: contrastAlgo == "dps",
|
||||
IsLightMode: isLight,
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
BackgroundDark: background,
|
||||
UseDPS: contrastAlgo == "dps",
|
||||
IsLightMode: isLight,
|
||||
}
|
||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||
fmt.Print(dank16.GenerateVariantJSON(variantColors))
|
||||
|
||||
@@ -95,6 +95,8 @@ var (
|
||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
||||
scrollVersionRegex = regexp.MustCompile(`scroll version (\d+\.\d+)`)
|
||||
aqueousVersionRegex = regexp.MustCompile(`(?i)aqueous v?(\d+\.\d+(?:\.\d+)?)`)
|
||||
)
|
||||
|
||||
var doctorCmd = &cobra.Command{
|
||||
@@ -739,6 +741,8 @@ func checkWindowManagers() []checkResult {
|
||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
||||
{"Scroll", "scroll", "--version", scrollVersionRegex, []string{"scroll"}},
|
||||
{"Aqueous", "aqueous", "-version", aqueousVersionRegex, []string{"aqueous"}},
|
||||
}
|
||||
|
||||
var results []checkResult
|
||||
@@ -771,7 +775,7 @@ func checkWindowManagers() []checkResult {
|
||||
results = append(results, checkResult{
|
||||
catCompositor, "Compositor", statusError,
|
||||
"No supported Wayland compositor found",
|
||||
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
||||
"Install Hyprland, niri, Sway, River, Wayfire, labwc, mangowc, miracle-wm, Scroll, or Aqueous",
|
||||
doctorDocsURL + "#compositor-checks",
|
||||
})
|
||||
}
|
||||
@@ -780,11 +784,28 @@ func checkWindowManagers() []checkResult {
|
||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||
}
|
||||
|
||||
if os.Getenv("AQUEOUS_SOCKET") != "" {
|
||||
results = append(results, checkAqueousConfigHelper())
|
||||
}
|
||||
|
||||
results = append(results, checkCompositorBlurSupport())
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func checkAqueousConfigHelper() checkResult {
|
||||
url := doctorDocsURL + "#compositor-checks"
|
||||
path, err := exec.LookPath("aqueous-config")
|
||||
if err != nil {
|
||||
return checkResult{catCompositor, "aqueous-config", statusWarn, "Not found", "Keybind and display settings edits from the shell need the aqueous-config helper", url}
|
||||
}
|
||||
details := "Keybind and display settings edits from the shell"
|
||||
if doctorVerbose {
|
||||
details = path
|
||||
}
|
||||
return checkResult{catCompositor, "aqueous-config", statusOK, "Installed", details, url}
|
||||
}
|
||||
|
||||
func checkCompositorBlurSupport() checkResult {
|
||||
supported, err := blur.ProbeSupport()
|
||||
if err != nil {
|
||||
@@ -817,6 +838,8 @@ func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||
|
||||
func detectRunningWM() string {
|
||||
switch {
|
||||
case os.Getenv("AQUEOUS_SOCKET") != "":
|
||||
return "Aqueous"
|
||||
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
|
||||
return "Hyprland"
|
||||
case os.Getenv("NIRI_SOCKET") != "":
|
||||
@@ -911,7 +934,7 @@ ShellRoot {
|
||||
{"Polkit", "Authentication prompts"},
|
||||
{"IdleMonitor", "Idle detection"},
|
||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||
{"ShortcutInhibitor", "Allow shortcut management (niri, Aqueous)"},
|
||||
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ var (
|
||||
ssJSON bool
|
||||
ssGeometry bool
|
||||
ssAllowMulti bool
|
||||
ssHUD string
|
||||
)
|
||||
|
||||
type screenshotMetadata struct {
|
||||
@@ -181,6 +182,7 @@ func init() {
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
|
||||
screenshotCmd.PersistentFlags().BoolVarP(&ssGeometry, "geometry", "g", false, "Print selected region geometry (X,Y WxH) to stdout without capturing an image")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssAllowMulti, "allow-multiple", false, "Open a selector even when another one is already open")
|
||||
screenshotCmd.PersistentFlags().StringVar(&ssHUD, "hud", "auto", "HUD overlay scale in region selector (auto, off/0, or integer scale 1-4)")
|
||||
|
||||
ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)")
|
||||
|
||||
@@ -212,6 +214,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config.Stdout = ssStdout
|
||||
config.Geometry = ssGeometry
|
||||
config.AllowMultiple = ssAllowMulti
|
||||
config.HUD = ssHUD
|
||||
|
||||
if ssGeometry {
|
||||
config.Clipboard = false
|
||||
|
||||
+7
-8
@@ -3,7 +3,7 @@ module github.com/AvengeMedia/DankMaterialShell/core
|
||||
go 1.26.5
|
||||
|
||||
require (
|
||||
github.com/AvengeMedia/dgop v0.2.4-0.20260819141338-085d828cf577
|
||||
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287
|
||||
github.com/Nadim147c/material/v3 v3.1.1
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
@@ -18,7 +18,7 @@ require (
|
||||
github.com/pilebones/go-udev v0.9.1
|
||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
github.com/yuin/goldmark v1.8.4
|
||||
@@ -71,9 +71,10 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect
|
||||
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||
@@ -84,10 +85,9 @@ require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/go-git/go-git/v6 v6.0.0-alpha.4
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -98,13 +98,12 @@ require (
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/xo/terminfo v1.0.0 // indirect
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/text v0.41.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
||||
+20
-22
@@ -2,8 +2,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea h1:A2IuXIzhTQwvBZiS8pxU5RmWSszMd16Oh11gUlJ9OeU=
|
||||
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea/go.mod h1:w1W/vDrWJCLdjVI4E/mURiKUQLMCmiGiPLxZEeP5kp8=
|
||||
github.com/AvengeMedia/dgop v0.2.4-0.20260819141338-085d828cf577 h1:z7daEy56wLH6R7PGS3MKIVoDHNdVoDptY/f2i61EDCQ=
|
||||
github.com/AvengeMedia/dgop v0.2.4-0.20260819141338-085d828cf577/go.mod h1:2kt1u9YgHwi2npqB5ago8kwZdFrHlPGXv3PM/vwRHfY=
|
||||
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287 h1:wzQqP16zxfUcfFltJelKg6pTdRyiLY2l2NJLT0Par+Q=
|
||||
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287/go.mod h1:lwzO5Y4BO5z0I7cLMp5A7sw/THliwGz1TNv6xoUehY4=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Nadim147c/material/v3 v3.1.1 h1:3ZuTPWBJWUpzzWW1rtqe28RMZXhA4f0/mg/Jk3JG94Y=
|
||||
@@ -38,8 +38,8 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
||||
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
@@ -61,8 +61,6 @@ github.com/danielgtaylor/huma/v2 v2.39.1 h1:0kwF4ltQoYZ+IU55VPy+BcGekzgF44R64daT
|
||||
github.com/danielgtaylor/huma/v2 v2.39.1/go.mod h1:zcnQ38duIJ3VUHwFaBoZ6x8T+KN/mr33oyqxcj0HTug=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
|
||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
|
||||
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
|
||||
@@ -85,8 +83,8 @@ github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
|
||||
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
|
||||
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
|
||||
@@ -166,8 +164,6 @@ github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -192,8 +188,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0 h1:CnIEL2n7Xql6Ux1k+Vu5S5ubDHCT/kxFgkKCY8FjefU=
|
||||
@@ -204,8 +200,8 @@ github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqo
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
|
||||
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||
@@ -219,18 +215,20 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM=
|
||||
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
||||
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
|
||||
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
|
||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
@@ -243,10 +241,10 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
|
||||
@@ -557,7 +557,7 @@ func TestKittyConfigStructure(t *testing.T) {
|
||||
func TestKittyThemeConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
|
||||
assert.Contains(t, KittyThemeConfig, "background #101418")
|
||||
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
|
||||
assert.Contains(t, KittyThemeConfig, "cursor #9dcbfb")
|
||||
assert.Contains(t, KittyThemeConfig, "color0 #101418")
|
||||
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cursor #e0e2e8
|
||||
cursor_text_color #c2c7cf
|
||||
cursor #9dcbfb
|
||||
cursor_text_color #003354
|
||||
|
||||
foreground #e0e2e8
|
||||
background #101418
|
||||
|
||||
@@ -3,21 +3,32 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Mirrors Hyprland's shipped target. Before=graphical-session.target would cycle with the add-wants implicit After=dms.service.
|
||||
const hyprlandSessionTargetUnit = `[Unit]
|
||||
Description=Hyprland Session Target
|
||||
BindsTo=graphical-session.target
|
||||
Wants=graphical-session-pre.target
|
||||
After=graphical-session-pre.target
|
||||
PropagatesStopTo=graphical-session.target
|
||||
`
|
||||
|
||||
var staleHyprlandSessionTargetUnits = []string{
|
||||
`[Unit]
|
||||
Description=Hyprland Session Target
|
||||
Requires=graphical-session.target
|
||||
After=graphical-session.target
|
||||
`,
|
||||
`[Unit]
|
||||
Description=Hyprland Session Target
|
||||
BindsTo=graphical-session.target
|
||||
Before=graphical-session.target
|
||||
Wants=graphical-session-pre.target
|
||||
After=graphical-session-pre.target
|
||||
`
|
||||
|
||||
const legacyHyprlandSessionTargetUnit = `[Unit]
|
||||
Description=Hyprland Session Target
|
||||
Requires=graphical-session.target
|
||||
After=graphical-session.target
|
||||
`
|
||||
`,
|
||||
}
|
||||
|
||||
// Only a missing or dankinstall-written unit is replaced; hand-written units are left alone.
|
||||
func EnsureHyprlandSessionTarget() (string, error) {
|
||||
@@ -30,10 +41,7 @@ func EnsureHyprlandSessionTarget() (string, error) {
|
||||
existing, err := os.ReadFile(targetPath)
|
||||
if err == nil {
|
||||
content := string(existing)
|
||||
if content == hyprlandSessionTargetUnit {
|
||||
return targetPath, nil
|
||||
}
|
||||
if content != legacyHyprlandSessionTargetUnit {
|
||||
if content == hyprlandSessionTargetUnit || !slices.Contains(staleHyprlandSessionTargetUnits, content) {
|
||||
return targetPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,23 +31,25 @@ func TestEnsureHyprlandSessionTargetWritesMissing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHyprlandSessionTargetUpgradesLegacy(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
path := targetPathFor(t, home)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(legacyHyprlandSessionTargetUnit), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
func TestEnsureHyprlandSessionTargetUpgradesStale(t *testing.T) {
|
||||
for _, stale := range staleHyprlandSessionTargetUnits {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
path := targetPathFor(t, home)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(stale), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := EnsureHyprlandSessionTarget(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := os.ReadFile(path)
|
||||
if string(data) != hyprlandSessionTargetUnit {
|
||||
t.Fatalf("legacy unit not upgraded:\n%s", data)
|
||||
if _, err := EnsureHyprlandSessionTarget(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := os.ReadFile(path)
|
||||
if string(data) != hyprlandSessionTargetUnit {
|
||||
t.Fatalf("stale unit not upgraded:\n%s", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -455,6 +455,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
|
||||
type PaletteOptions struct {
|
||||
IsLight bool
|
||||
Background string
|
||||
Container string
|
||||
UseDPS bool
|
||||
}
|
||||
|
||||
@@ -520,8 +521,18 @@ func DeriveContainer(primary string, isLight bool) string {
|
||||
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||
}
|
||||
|
||||
// DeriveContainer only approximates the Material tone pairing, so a caller that
|
||||
// has the real primary_container should pass it: color5 and color6 are the
|
||||
// primary/container pair and their contrast with each other is the invariant.
|
||||
func resolveContainer(primaryColor string, opts PaletteOptions) string {
|
||||
if opts.Container != "" {
|
||||
return opts.Container
|
||||
}
|
||||
return DeriveContainer(primaryColor, opts.IsLight)
|
||||
}
|
||||
|
||||
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
baseColor := DeriveContainer(primaryColor, opts.IsLight)
|
||||
baseColor := resolveContainer(primaryColor, opts)
|
||||
|
||||
rgb := HexToRGB(baseColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -531,13 +542,20 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
|
||||
var palette Palette
|
||||
|
||||
var normalTextTarget, secondaryTarget float64
|
||||
if opts.UseDPS {
|
||||
// Delta Phi Star is not symmetric across polarity: on a light background the
|
||||
// same Lc buys far less real contrast, so the light targets are the numbers
|
||||
// that land on the same measured WCAG ratios the dark ones already deliver.
|
||||
var normalTextTarget, accentTarget float64
|
||||
switch {
|
||||
case opts.UseDPS && opts.IsLight:
|
||||
normalTextTarget = 70.0
|
||||
accentTarget = 58.0
|
||||
case opts.UseDPS:
|
||||
normalTextTarget = 40.0
|
||||
secondaryTarget = 35.0
|
||||
} else {
|
||||
accentTarget = 24.5
|
||||
default:
|
||||
normalTextTarget = 4.5
|
||||
secondaryTarget = 3.0
|
||||
accentTarget = 2.1
|
||||
}
|
||||
|
||||
var bgColor string
|
||||
@@ -557,8 +575,6 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
greenH := blendHue(0.33, ph.H, 0.10)
|
||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||
|
||||
accentTarget := secondaryTarget * 0.7
|
||||
|
||||
if opts.IsLight {
|
||||
redS := math.Min(baseSat*1.2, 1.0)
|
||||
redV := baseVal * 0.95
|
||||
@@ -570,15 +586,13 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
|
||||
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
blueS := math.Min(ph.S*1.05, 1.0)
|
||||
blueV := math.Min(ph.V*1.05, 1.0)
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (light container in light mode)
|
||||
container5 := DeriveContainer(primaryColor, true)
|
||||
palette.Color5 = NewColorInfo(container5)
|
||||
palette.Color5 = NewColorInfo(baseColor)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
@@ -604,8 +618,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
lightContainer := DeriveContainer(primaryColor, true)
|
||||
palette.Color13 = NewColorInfo(lightContainer)
|
||||
color13S := ph.S * 0.7
|
||||
color13V := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color13 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})), bgColor, accentTarget, opts))
|
||||
|
||||
brightCyanS := ph.S * 0.5
|
||||
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||
@@ -632,9 +647,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
blueV := ph.V * 0.95
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||
darkContainer := DeriveContainer(primaryColor, false)
|
||||
palette.Color5 = NewColorInfo(darkContainer)
|
||||
palette.Color5 = NewColorInfo(baseColor)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
@@ -681,11 +694,14 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
}
|
||||
|
||||
type VariantOptions struct {
|
||||
PrimaryDark string
|
||||
PrimaryLight string
|
||||
Background string
|
||||
UseDPS bool
|
||||
IsLightMode bool
|
||||
PrimaryDark string
|
||||
PrimaryLight string
|
||||
BackgroundDark string
|
||||
BackgroundLight string
|
||||
ContainerDark string
|
||||
ContainerLight string
|
||||
UseDPS bool
|
||||
IsLightMode bool
|
||||
}
|
||||
|
||||
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
|
||||
@@ -702,8 +718,8 @@ func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
|
||||
}
|
||||
|
||||
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
|
||||
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||
darkOpts := PaletteOptions{IsLight: false, Background: opts.BackgroundDark, Container: opts.ContainerDark, UseDPS: opts.UseDPS}
|
||||
lightOpts := PaletteOptions{IsLight: true, Background: opts.BackgroundLight, Container: opts.ContainerLight, UseDPS: opts.UseDPS}
|
||||
|
||||
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
|
||||
light := GeneratePalette(opts.PrimaryLight, lightOpts)
|
||||
|
||||
@@ -701,7 +701,8 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||
|
||||
bgColor := result.Color0.Hex
|
||||
for i := 1; i < 8; i++ {
|
||||
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||
// Color5 is the container and Color6 the exact primary: they pair
|
||||
// with each other, not with the background
|
||||
if i == 5 || i == 6 {
|
||||
continue
|
||||
}
|
||||
@@ -862,3 +863,112 @@ func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
|
||||
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteLightYellowMeetsNormalTextContrast(t *testing.T) {
|
||||
primaries := []string{"#63568f", "#7d5800", "#116d2b", "#1c5070", "#8b3f68"}
|
||||
|
||||
for _, primary := range primaries {
|
||||
t.Run(primary, func(t *testing.T) {
|
||||
palette := GeneratePalette(primary, PaletteOptions{IsLight: true, Background: "#f5fafc", UseDPS: true})
|
||||
|
||||
ratio := ContrastRatio(palette.Color3.Hex, palette.Color0.Hex)
|
||||
if ratio < 4.5 {
|
||||
t.Errorf("light Color3 %s vs bg %s ratio %.2f, expected >= 4.5 (yellow must clear the normal-text target)",
|
||||
palette.Color3.Hex, palette.Color0.Hex, ratio)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVariantPaletteUsesPerVariantBackground(t *testing.T) {
|
||||
const darkBg = "#0f1417"
|
||||
const lightBg = "#f5fafc"
|
||||
|
||||
variants := GenerateVariantPalette(VariantOptions{
|
||||
PrimaryDark: "#8ecff2",
|
||||
PrimaryLight: "#1c5070",
|
||||
BackgroundDark: darkBg,
|
||||
BackgroundLight: lightBg,
|
||||
UseDPS: true,
|
||||
})
|
||||
|
||||
if variants.Color0.Dark.Hex != darkBg {
|
||||
t.Errorf("dark Color0 = %s, expected the dark background %s", variants.Color0.Dark.Hex, darkBg)
|
||||
}
|
||||
if variants.Color0.Light.Hex != lightBg {
|
||||
t.Errorf("light Color0 = %s, expected the light background %s", variants.Color0.Light.Hex, lightBg)
|
||||
}
|
||||
|
||||
lightDim := variants.Color8.Light.Hex
|
||||
if lstar := getLstar(lightDim); lstar < 60.0 {
|
||||
t.Errorf("light Color8 %s has L* %.2f, expected >= 60 (derived from the dark surface it collapses to near-black)",
|
||||
lightDim, lstar)
|
||||
}
|
||||
if ratio := ContrastRatio(lightDim, lightBg); ratio > 3.0 {
|
||||
t.Errorf("light Color8 %s vs bg %s ratio %.2f, expected <= 3.0 (bright black stays dim)", lightDim, lightBg, ratio)
|
||||
}
|
||||
|
||||
darkDim := variants.Color8.Dark.Hex
|
||||
if ratio := ContrastRatio(darkDim, darkBg); ratio > 3.0 {
|
||||
t.Errorf("dark Color8 %s vs bg %s ratio %.2f, expected <= 3.0", darkDim, darkBg, ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteColor5PairsWithColor6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
primary string
|
||||
container string
|
||||
bg string
|
||||
isLight bool
|
||||
}{
|
||||
{"violet dark", "#cdbdff", "#4b3e76", "#141318", false},
|
||||
{"violet light", "#63568f", "#e8deff", "#fdf7ff", true},
|
||||
{"green dark", "#9ed49d", "#1f5027", "#101510", false},
|
||||
{"green light", "#38693c", "#b9f0b8", "#f7fbf2", true},
|
||||
{"amber light", "#805610", "#ffddb3", "#fff8f4", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
palette := GeneratePalette(tt.primary, PaletteOptions{
|
||||
IsLight: tt.isLight,
|
||||
Background: tt.bg,
|
||||
Container: tt.container,
|
||||
UseDPS: true,
|
||||
})
|
||||
|
||||
if palette.Color5.Hex != tt.container {
|
||||
t.Errorf("Color5 = %s, expected the supplied container %s", palette.Color5.Hex, tt.container)
|
||||
}
|
||||
if palette.Color6.Hex != tt.primary {
|
||||
t.Errorf("Color6 = %s, expected the primary %s", palette.Color6.Hex, tt.primary)
|
||||
}
|
||||
|
||||
pair := ContrastRatio(palette.Color5.Hex, palette.Color6.Hex)
|
||||
if pair < 4.5 {
|
||||
t.Errorf("Color5 %s vs Color6 %s ratio %.2f, expected >= 4.5 (the container/primary pair carries the prompt themes that use ANSI 5 on 6)",
|
||||
palette.Color5.Hex, palette.Color6.Hex, pair)
|
||||
}
|
||||
|
||||
if palette.Color5.Hex == palette.Color13.Hex {
|
||||
t.Errorf("Color5 and Color13 are both %s, expected a distinct bright variant", palette.Color5.Hex)
|
||||
}
|
||||
if sep := ContrastRatio(palette.Color5.Hex, palette.Color13.Hex); sep < 2.0 {
|
||||
t.Errorf("Color5 %s vs Color13 %s ratio %.2f, expected >= 2.0 (normal and bright must be tellable apart)",
|
||||
palette.Color5.Hex, palette.Color13.Hex, sep)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteFallsBackToDerivedContainer(t *testing.T) {
|
||||
const primary = "#cdbdff"
|
||||
|
||||
palette := GeneratePalette(primary, PaletteOptions{Background: "#141318", UseDPS: true})
|
||||
|
||||
want := DeriveContainer(primary, false)
|
||||
if palette.Color5.Hex != want {
|
||||
t.Errorf("Color5 = %s with no container supplied, expected the derived %s", palette.Color5.Hex, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
|
||||
var primaryDark, primaryLight, surface string
|
||||
var primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight string
|
||||
var dank16JSON string
|
||||
var importArgs []string
|
||||
var sourceImage string
|
||||
@@ -373,7 +373,10 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
log.Info("Using stock/custom theme colors with matugen base")
|
||||
primaryDark = extractNestedColor(opts.StockColors, "primary", "dark")
|
||||
primaryLight = extractNestedColor(opts.StockColors, "primary", "light")
|
||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
surfaceDark = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
surfaceLight = extractNestedColor(opts.StockColors, "surface", "light")
|
||||
containerDark = extractNestedColor(opts.StockColors, "primary_container", "dark")
|
||||
containerLight = extractNestedColor(opts.StockColors, "primary_container", "light")
|
||||
|
||||
if primaryDark == "" {
|
||||
return false, fmt.Errorf("failed to extract primary dark from stock colors")
|
||||
@@ -382,7 +385,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
primaryLight = primaryDark
|
||||
}
|
||||
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight, opts.Mode)
|
||||
importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON)
|
||||
importArgs = []string{"--import-json-string", importData}
|
||||
|
||||
@@ -403,7 +406,10 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||
primaryLight = extractMatugenColor(matJSON, "primary", "light")
|
||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||
surfaceDark = extractMatugenColor(matJSON, "surface", "dark")
|
||||
surfaceLight = extractMatugenColor(matJSON, "surface", "light")
|
||||
containerDark = extractMatugenColor(matJSON, "primary_container", "dark")
|
||||
containerLight = extractMatugenColor(matJSON, "primary_container", "light")
|
||||
|
||||
if primaryDark == "" {
|
||||
return false, fmt.Errorf("failed to extract primary color")
|
||||
@@ -412,7 +418,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
primaryLight = primaryDark
|
||||
}
|
||||
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight, opts.Mode)
|
||||
importArgs = []string{"--import-json-string", buildImportData(dank16JSON, sourceImage)}
|
||||
|
||||
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
|
||||
@@ -1125,13 +1131,16 @@ func extractNestedColor(jsonStr, colorName, variant string) string {
|
||||
return color
|
||||
}
|
||||
|
||||
func generateDank16Variants(primaryDark, primaryLight, surface string, mode ColorMode) string {
|
||||
func generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight string, mode ColorMode) string {
|
||||
variantOpts := dank16.VariantOptions{
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
Background: surface,
|
||||
UseDPS: true,
|
||||
IsLightMode: mode == ColorModeLight,
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
BackgroundDark: surfaceDark,
|
||||
BackgroundLight: surfaceLight,
|
||||
ContainerDark: containerDark,
|
||||
ContainerLight: containerLight,
|
||||
UseDPS: true,
|
||||
IsLightMode: mode == ColorModeLight,
|
||||
}
|
||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||
return dank16.GenerateVariantJSON(variantColors)
|
||||
|
||||
@@ -3,6 +3,7 @@ package qmlchecks
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -55,3 +56,51 @@ func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
|
||||
t.Fatalf("DMS-managed policy must generate the sanitized user PAM stack on Nix-store installs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockScreenFprintProfileMatchesPamQml(t *testing.T) {
|
||||
profile, err := os.ReadFile("../../../quickshell/assets/pam/fprint")
|
||||
if err != nil {
|
||||
t.Fatalf("read fprint PAM profile: %v", err)
|
||||
}
|
||||
|
||||
qml, err := os.ReadFile("../../../quickshell/Modules/Lock/Pam.qml")
|
||||
if err != nil {
|
||||
t.Fatalf("read lock screen PAM QML: %v", err)
|
||||
}
|
||||
|
||||
// Pam.qml tells a pam_fprintd timeout expiry apart from a device fault by
|
||||
// how long the session ran, so a profile change that is not mirrored in
|
||||
// sessionTimeoutMs makes real faults look like planned renewals.
|
||||
profileTimeout := regexp.MustCompile(`pam_fprintd\.so[^\n]*\btimeout=(\d+)`).FindSubmatch(profile)
|
||||
if profileTimeout == nil {
|
||||
t.Fatalf("fprint profile must set an explicit pam_fprintd timeout")
|
||||
}
|
||||
qmlTimeout := regexp.MustCompile(`sessionTimeoutMs:\s*(\d+)`).FindSubmatch(qml)
|
||||
if qmlTimeout == nil {
|
||||
t.Fatalf("Pam.qml must declare sessionTimeoutMs")
|
||||
}
|
||||
|
||||
seconds, err := strconv.Atoi(string(profileTimeout[1]))
|
||||
if err != nil {
|
||||
t.Fatalf("parse profile timeout: %v", err)
|
||||
}
|
||||
millis, err := strconv.Atoi(string(qmlTimeout[1]))
|
||||
if err != nil {
|
||||
t.Fatalf("parse sessionTimeoutMs: %v", err)
|
||||
}
|
||||
if seconds*1000 != millis {
|
||||
t.Fatalf("sessionTimeoutMs is %dms but the fprint profile sets timeout=%d", millis, seconds)
|
||||
}
|
||||
|
||||
// fprintd 1.94.0 and older ignore this option when it is longer than two
|
||||
// digits and silently fall back to 30 seconds.
|
||||
if len(profileTimeout[1]) > 2 {
|
||||
t.Fatalf("pam_fprintd timeout=%s is too long for fprintd <= 1.94.0, which would ignore it", profileTimeout[1])
|
||||
}
|
||||
|
||||
// One finished session must equal one wrong finger, otherwise
|
||||
// SettingsData.maxFprintTries silently means a multiple of the scans it names.
|
||||
if !regexp.MustCompile(`pam_fprintd\.so[^\n]*\bmax-tries=1\b`).Match(profile) {
|
||||
t.Fatalf("fprint profile must use max-tries=1 so the shell counts wrong scans one for one")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -973,11 +973,12 @@ func (r *RegionSelector) renderSurface(os *OutputSurface) {
|
||||
case !slot.cacheValid(srcBuf, r.selection.dragging, r.showCapturedCursor, r.phase, handles, shift):
|
||||
slot.shm.CopyFrom(srcBuf)
|
||||
r.dimBackground(slot.shm)
|
||||
scale := 1
|
||||
effectiveScale := 1.0
|
||||
if os.output != nil {
|
||||
scale = scaleFactor(os.output.effectiveScale())
|
||||
effectiveScale = os.output.effectiveScale()
|
||||
}
|
||||
r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat, scale)
|
||||
hudScale := r.hudScale(effectiveScale)
|
||||
r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat, hudScale)
|
||||
slot.backgroundInitialized = true
|
||||
slot.backgroundSource = srcBuf
|
||||
slot.backgroundDragging = r.selection.dragging
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -630,6 +632,32 @@ func (r *RegionSelector) drawScrollBar(data []byte, stride, bufW, bufH int, form
|
||||
style.TextR, style.TextG, style.TextB, format)
|
||||
}
|
||||
|
||||
const (
|
||||
minHUDScale = 1
|
||||
maxHUDScale = 4
|
||||
)
|
||||
|
||||
func (r *RegionSelector) hudScale(effectiveScale float64) int {
|
||||
hudOpt := "auto"
|
||||
if r.screenshoter != nil && r.screenshoter.config.HUD != "" {
|
||||
hudOpt = strings.ToLower(strings.TrimSpace(r.screenshoter.config.HUD))
|
||||
}
|
||||
switch hudOpt {
|
||||
case "off", "none", "false", "0":
|
||||
return 0
|
||||
case "", "auto":
|
||||
return min(scaleFactor(effectiveScale), maxHUDScale)
|
||||
default:
|
||||
if val, err := strconv.ParseFloat(hudOpt, 64); err == nil {
|
||||
if val <= 0 {
|
||||
return 0
|
||||
}
|
||||
return min(max(scaleFactor(val), minHUDScale), maxHUDScale)
|
||||
}
|
||||
return min(scaleFactor(effectiveScale), maxHUDScale)
|
||||
}
|
||||
}
|
||||
|
||||
type hudItem struct {
|
||||
key, desc string
|
||||
}
|
||||
@@ -657,6 +685,9 @@ func (r *RegionSelector) hudItems() []hudItem {
|
||||
}
|
||||
|
||||
func (r *RegionSelector) hudDimensions(bufW, bufH, scale int) (hudX, hudY, hudW, hudH int) {
|
||||
if scale <= 0 {
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
scale = scaleFactor(float64(scale))
|
||||
charH := fontCharH * scale
|
||||
padding, itemSpacing := 12*scale, 24*scale
|
||||
@@ -680,7 +711,7 @@ func (r *RegionSelector) hudDimensions(bufW, bufH, scale int) (hudX, hudY, hudW,
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32, scale int) {
|
||||
if r.selection.dragging {
|
||||
if r.selection.dragging || scale <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -238,6 +238,12 @@ func TestHUDDimensionsContainText(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// When scale <= 0, hudDimensions should return zero
|
||||
hx, hy, hw, hh := r.hudDimensions(bufW, bufH, 0)
|
||||
if hx != 0 || hy != 0 || hw != 0 || hh != 0 {
|
||||
t.Fatalf("expected 0 dimensions for scale 0, got %d, %d, %d, %d", hx, hy, hw, hh)
|
||||
}
|
||||
|
||||
for _, scale := range []int{1, 2} {
|
||||
hudX, hudY, hudW, hudH := r.hudDimensions(bufW, bufH, scale)
|
||||
data := make([]byte, bufH*stride)
|
||||
@@ -260,6 +266,47 @@ func TestHUDDimensionsContainText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHUDScale(t *testing.T) {
|
||||
tests := []struct {
|
||||
hudOpt string
|
||||
effectiveScale float64
|
||||
expectedScale int
|
||||
}{
|
||||
{"auto", 1.0, 1},
|
||||
{"auto", 1.8, 2},
|
||||
{"auto", 2.4, 2},
|
||||
{"", 1.0, 1},
|
||||
{"", 2.0, 2},
|
||||
{"off", 1.0, 0},
|
||||
{"OFF", 2.0, 0},
|
||||
{"none", 1.5, 0},
|
||||
{"false", 1.0, 0},
|
||||
{"0", 2.0, 0},
|
||||
{"0.0", 1.0, 0},
|
||||
{"-1", 1.0, 0},
|
||||
{"1", 1.0, 1},
|
||||
{"1.5", 1.0, 2},
|
||||
{"2", 1.0, 2},
|
||||
{"3", 1.0, 3},
|
||||
{"4", 1.0, 4},
|
||||
{"1000", 1.0, 4},
|
||||
{"0.2", 1.0, 1},
|
||||
{"invalid", 1.7, 2},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
r := &RegionSelector{
|
||||
screenshoter: &Screenshoter{
|
||||
config: Config{HUD: tc.hudOpt},
|
||||
},
|
||||
}
|
||||
got := r.hudScale(tc.effectiveScale)
|
||||
if got != tc.expectedScale {
|
||||
t.Errorf("hudScale(%q, %v) = %d, expected %d", tc.hudOpt, tc.effectiveScale, got, tc.expectedScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayDeltaHandlesClampedMonitorEdge(t *testing.T) {
|
||||
prev := &overlay{
|
||||
interior: dirtyRect{100, 100, 1920, 1080},
|
||||
|
||||
@@ -73,6 +73,7 @@ type Config struct {
|
||||
Geometry bool
|
||||
AllowMultiple bool
|
||||
IntervalMs int
|
||||
HUD string
|
||||
// SelectorHook runs as the interactive selector starts (true) and ends (false).
|
||||
SelectorHook func(begin bool)
|
||||
}
|
||||
@@ -90,5 +91,6 @@ func DefaultConfig() Config {
|
||||
Clipboard: true,
|
||||
SaveFile: true,
|
||||
Notify: true,
|
||||
HUD: "auto",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ const (
|
||||
propertiesIface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
|
||||
// Sender-scoped: the system bus is shared process-wide, so unscoped rules wake
|
||||
// every other subsystem's signal channel too.
|
||||
var bluezMatchRules = [][]dbus.MatchOption{
|
||||
{dbus.WithMatchInterface(propertiesIface), dbus.WithMatchMember("PropertiesChanged")},
|
||||
{dbus.WithMatchInterface(objectMgrIface), dbus.WithMatchMember("InterfacesAdded")},
|
||||
{dbus.WithMatchInterface(objectMgrIface), dbus.WithMatchMember("InterfacesRemoved")},
|
||||
{dbus.WithMatchSender(bluezService), dbus.WithMatchInterface(propertiesIface), dbus.WithMatchMember("PropertiesChanged")},
|
||||
{dbus.WithMatchSender(bluezService), dbus.WithMatchInterface(objectMgrIface), dbus.WithMatchMember("InterfacesAdded")},
|
||||
{dbus.WithMatchSender(bluezService), dbus.WithMatchInterface(objectMgrIface), dbus.WithMatchMember("InterfacesRemoved")},
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
|
||||
@@ -19,7 +19,7 @@ func WaitForAdapter() error {
|
||||
conn.Signal(signals)
|
||||
defer conn.RemoveSignal(signals)
|
||||
|
||||
adapterAdded := []dbus.MatchOption{dbus.WithMatchInterface(objectMgrIface), dbus.WithMatchMember("InterfacesAdded")}
|
||||
adapterAdded := []dbus.MatchOption{dbus.WithMatchSender(bluezService), dbus.WithMatchInterface(objectMgrIface), dbus.WithMatchMember("InterfacesAdded")}
|
||||
if err := conn.AddMatchSignal(adapterAdded...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package brightness
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
@@ -20,13 +21,40 @@ const (
|
||||
DDCCI_VCP_SET = 0x03
|
||||
VCP_BRIGHTNESS = 0x10
|
||||
DDC_SOURCE_ADDR = 0x51
|
||||
EDID_ADDR = 0x50
|
||||
)
|
||||
|
||||
// ddcutil's DEFAULT_FLOCK_POLL_MILLISEC and DEFAULT_FLOCK_MAX_WAIT_MILLISEC (src/base/parms.h).
|
||||
const (
|
||||
ddcBusLockPoll = 100 * time.Millisecond
|
||||
ddcBusLockWait = 3 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
errDDCDisabled = errors.New("disabled via DMS_NO_DDC")
|
||||
errInvalidEDID = errors.New("invalid edid")
|
||||
errDisplayAsleep = errors.New("display asleep")
|
||||
errNoMonitor = errors.New("no monitor on bus")
|
||||
errBusReused = errors.New("bus belongs to another adapter")
|
||||
)
|
||||
|
||||
var ddcRereadDelays = []time.Duration{5 * time.Second, 10 * time.Second, 20 * time.Second}
|
||||
|
||||
func NewDDCBackend() (*DDCBackend, error) {
|
||||
return newDDCBackend(nil)
|
||||
}
|
||||
|
||||
func newDDCBackend(onReread func()) (*DDCBackend, error) {
|
||||
if os.Getenv("DMS_NO_DDC") != "" {
|
||||
return nil, errDDCDisabled
|
||||
}
|
||||
|
||||
b := &DDCBackend{
|
||||
scanInterval: 30 * time.Second,
|
||||
debounceTimers: make(map[string]*time.Timer),
|
||||
debouncePending: make(map[string]ddcPendingSet),
|
||||
onReread: onReread,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
if err := b.scanI2CDevices(); err != nil {
|
||||
@@ -53,6 +81,7 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
||||
}
|
||||
|
||||
activeBuses := make(map[int]bool)
|
||||
connectors := drmConnectorsByBus()
|
||||
|
||||
for i := range 32 {
|
||||
busPath := fmt.Sprintf("/dev/i2c-%d", i)
|
||||
@@ -70,18 +99,36 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
||||
|
||||
// Don't re-probe identified monitors: DDC traffic during a wake
|
||||
// sequence can disturb some monitors' own brightness handling.
|
||||
if _, ok := b.devices.Load(id); ok {
|
||||
if known, ok := b.devices.Load(id); ok {
|
||||
if known.adapter == getI2CDeviceSysfsName(i) {
|
||||
continue
|
||||
}
|
||||
b.devices.Delete(id)
|
||||
log.Debugf("removed DDC device %s (bus number reused by another adapter)", id)
|
||||
}
|
||||
|
||||
verdict := ddcBusVerdictFor(i, connectors)
|
||||
if verdict == ddcBusSkip {
|
||||
log.Debugf("i2c-%d: no monitor per DRM sysfs, not probing", i)
|
||||
continue
|
||||
}
|
||||
if ddcDisplayAsleep(i, connectors) {
|
||||
log.Debugf("i2c-%d: display asleep, not probing", i)
|
||||
continue
|
||||
}
|
||||
|
||||
dev, err := b.probeDDCDevice(i)
|
||||
dev, err := b.probeDDCDevice(i, verdict == ddcBusNeedsEDIDRead)
|
||||
if err != nil || dev == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dev.id = id
|
||||
unread := dev.max == 0
|
||||
b.devices.Store(id, dev)
|
||||
log.Debugf("found DDC device on i2c-%d", i)
|
||||
if unread && b.onReread != nil {
|
||||
go b.rereadUntilKnown(dev)
|
||||
}
|
||||
}
|
||||
|
||||
b.devices.Range(func(id string, dev *ddcDevice) bool {
|
||||
@@ -97,53 +144,119 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) {
|
||||
busPath := fmt.Sprintf("/dev/i2c-%d", bus)
|
||||
|
||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||
func (b *DDCBackend) probeDDCDevice(bus int, readEDID bool) (*ddcDevice, error) {
|
||||
fd, err := openDDCBus(bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
dummy := make([]byte, 32)
|
||||
syscall.Read(fd, dummy) //nolint:errcheck
|
||||
|
||||
writebuf := []byte{DDC_SOURCE_ADDR, 0x80}
|
||||
writebuf = append(writebuf, ddcciChecksum(writebuf))
|
||||
n, err := syscall.Write(fd, writebuf)
|
||||
if err == nil && n == len(writebuf) {
|
||||
name := b.getDDCName(bus)
|
||||
dev := &ddcDevice{
|
||||
bus: bus,
|
||||
addr: DDCCI_ADDR,
|
||||
name: name,
|
||||
if readEDID {
|
||||
edid, err := readBusEDID(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isLaptopEDID(edid) {
|
||||
return nil, errors.New("laptop panel")
|
||||
}
|
||||
b.readInitialBrightness(fd, dev)
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
readbuf := make([]byte, 4)
|
||||
n, err = syscall.Read(fd, readbuf)
|
||||
if err != nil || n == 0 {
|
||||
if err := setI2CAddr(fd, DDCCI_ADDR); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !detectX37(fd) {
|
||||
return nil, fmt.Errorf("x37 unresponsive")
|
||||
}
|
||||
|
||||
name := b.getDDCName(bus)
|
||||
|
||||
dev := &ddcDevice{
|
||||
bus: bus,
|
||||
addr: DDCCI_ADDR,
|
||||
name: name,
|
||||
bus: bus,
|
||||
addr: DDCCI_ADDR,
|
||||
name: b.getDDCName(bus),
|
||||
adapter: getI2CDeviceSysfsName(bus),
|
||||
}
|
||||
b.readInitialBrightness(fd, dev)
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
// Based on ddcutil's i2c_open_bus() cross-instance lock (src/base/flock.c): ddcutil, other dms
|
||||
// processes and this one take turns on a bus.
|
||||
func openDDCBus(bus int) (int, error) {
|
||||
// Without O_CLOEXEC a process spawned mid-probe inherits the fd and the flock with it.
|
||||
fd, err := syscall.Open(fmt.Sprintf("/dev/i2c-%d", bus), syscall.O_RDWR|syscall.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
deadline := time.Now().Add(ddcBusLockWait)
|
||||
for {
|
||||
err = unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB)
|
||||
if err == nil {
|
||||
return fd, nil
|
||||
}
|
||||
if !errors.Is(err, unix.EWOULDBLOCK) || time.Now().After(deadline) {
|
||||
syscall.Close(fd)
|
||||
return -1, fmt.Errorf("lock i2c-%d: %w", bus, err)
|
||||
}
|
||||
time.Sleep(ddcBusLockPoll)
|
||||
}
|
||||
}
|
||||
|
||||
func setI2CAddr(fd, addr int) error {
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(addr)); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Based on ddcutil's i2c_detect_x37() (src/i2c/i2c_x37.c).
|
||||
func detectX37(fd int) bool {
|
||||
if n, err := syscall.Read(fd, make([]byte, 1)); err == nil && n == 1 {
|
||||
return true
|
||||
}
|
||||
_, err := syscall.Write(fd, nil)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Based on ddcutil's i2c_get_raw_edid_by_fd() with the fileio reader (src/i2c/i2c_edid.c).
|
||||
func readBusEDID(fd int) ([]byte, error) {
|
||||
if err := setI2CAddr(fd, EDID_ADDR); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var err error
|
||||
consecutiveEIO := 0
|
||||
for _, size := range []int{128, 128, 256, 256} {
|
||||
edid := make([]byte, size)
|
||||
if err = readEDIDOnce(fd, edid); err == nil {
|
||||
return edid[:128], nil
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, syscall.ENXIO), errors.Is(err, syscall.EOPNOTSUPP), errors.Is(err, syscall.ETIMEDOUT):
|
||||
return nil, err
|
||||
case errors.Is(err, syscall.EIO):
|
||||
consecutiveEIO++
|
||||
if consecutiveEIO >= 2 {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
consecutiveEIO = 0
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func readEDIDOnce(fd int, edid []byte) error {
|
||||
if _, err := syscall.Write(fd, []byte{0x00}); err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := syscall.Read(fd, edid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < 128 || !isValidEDID(edid[:128]) {
|
||||
return errInvalidEDID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) getDDCName(bus int) string {
|
||||
sysfsPath := fmt.Sprintf("/sys/class/i2c-adapter/i2c-%d/name", bus)
|
||||
data, err := os.ReadFile(sysfsPath)
|
||||
@@ -176,6 +289,63 @@ func (b *DDCBackend) readInitialBrightness(fd int, dev *ddcDevice) {
|
||||
}
|
||||
}
|
||||
|
||||
// Bounded and spaced: extra DDC traffic can upset some monitors (#2049).
|
||||
func (b *DDCBackend) rereadUntilKnown(dev *ddcDevice) {
|
||||
for _, delay := range ddcRereadDelays {
|
||||
select {
|
||||
case <-b.stop:
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
read, retry := b.rereadBrightness(dev)
|
||||
if read {
|
||||
b.onReread()
|
||||
return
|
||||
}
|
||||
if !retry {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Debugf("giving up on reading brightness for %s", dev.id)
|
||||
}
|
||||
|
||||
func (b *DDCBackend) rereadBrightness(dev *ddcDevice) (read, retry bool) {
|
||||
b.ioMutex.Lock()
|
||||
defer b.ioMutex.Unlock()
|
||||
|
||||
if current, ok := b.devices.Load(dev.id); !ok || current != dev || dev.max > 0 {
|
||||
return false, false
|
||||
}
|
||||
switch err := b.checkBusAlive(dev); {
|
||||
case errors.Is(err, errBusReused):
|
||||
return false, false
|
||||
case err != nil:
|
||||
return false, true
|
||||
}
|
||||
|
||||
fd, err := openDDCBus(dev.bus)
|
||||
if err != nil {
|
||||
log.Debugf("brightness reread skipped for %s: %v", dev.id, err)
|
||||
return false, true
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
if err := setI2CAddr(fd, dev.addr); err != nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS)
|
||||
if err != nil {
|
||||
log.Debugf("brightness reread failed for %s: %v", dev.id, err)
|
||||
return false, true
|
||||
}
|
||||
|
||||
dev.max = cap.max
|
||||
dev.lastBrightness = cap.current
|
||||
log.Debugf("read %s brightness %d/%d", dev.id, cap.current, cap.max)
|
||||
return true, false
|
||||
}
|
||||
|
||||
func (b *DDCBackend) GetDevices() ([]Device, error) {
|
||||
if err := b.scanI2CDevices(); err != nil {
|
||||
log.Debugf("DDC scan error: %v", err)
|
||||
@@ -183,6 +353,8 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
|
||||
|
||||
devices := make([]Device, 0)
|
||||
|
||||
b.ioMutex.Lock()
|
||||
defer b.ioMutex.Unlock()
|
||||
b.devices.Range(func(id string, dev *ddcDevice) bool {
|
||||
devices = append(devices, Device{
|
||||
Class: ClassDDC,
|
||||
@@ -190,7 +362,7 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
|
||||
Name: dev.name,
|
||||
Current: dev.lastBrightness,
|
||||
Max: dev.max,
|
||||
CurrentPercent: dev.lastBrightness,
|
||||
CurrentPercent: ddcPercentFromRaw(dev.lastBrightness, dev.max),
|
||||
Backend: "ddc",
|
||||
})
|
||||
return true
|
||||
@@ -274,6 +446,9 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
|
||||
return fmt.Errorf("device not found: %s", id)
|
||||
}
|
||||
|
||||
b.ioMutex.Lock()
|
||||
defer b.ioMutex.Unlock()
|
||||
|
||||
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
||||
|
||||
if _, err := os.Stat(busPath); os.IsNotExist(err) {
|
||||
@@ -282,52 +457,85 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
|
||||
return fmt.Errorf("device disconnected: %s", id)
|
||||
}
|
||||
|
||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
switch err := b.checkBusAlive(dev); {
|
||||
case errors.Is(err, errBusReused):
|
||||
b.devices.Delete(id)
|
||||
log.Debugf("removed DDC device %s (bus number reused by another adapter)", id)
|
||||
return err
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := openDDCBus(dev.bus)
|
||||
switch {
|
||||
case errors.Is(err, unix.EWOULDBLOCK):
|
||||
return err
|
||||
case err != nil:
|
||||
b.devices.Delete(id)
|
||||
log.Debugf("removed DDC device %s (open failed: %v)", id, err)
|
||||
return fmt.Errorf("open i2c device: %w", err)
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(dev.addr)); errno != 0 {
|
||||
return fmt.Errorf("set i2c slave addr: %w", errno)
|
||||
if err := setI2CAddr(fd, dev.addr); err != nil {
|
||||
return fmt.Errorf("set i2c slave addr: %w", err)
|
||||
}
|
||||
|
||||
max := dev.max
|
||||
if max == 0 {
|
||||
maxValue := dev.max
|
||||
if maxValue == 0 {
|
||||
cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current capability: %w", err)
|
||||
}
|
||||
max = cap.max
|
||||
dev.max = max
|
||||
b.devices.Store(id, dev)
|
||||
maxValue = cap.max
|
||||
}
|
||||
if maxValue <= 0 {
|
||||
return fmt.Errorf("%s reported max brightness %d", id, maxValue)
|
||||
}
|
||||
|
||||
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil {
|
||||
raw := ddcRawFromPercent(value, maxValue)
|
||||
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, raw); err != nil {
|
||||
return fmt.Errorf("set vcp feature: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("set %s to %d/%d", id, value, max)
|
||||
log.Debugf("set %s to %d/%d", id, raw, maxValue)
|
||||
|
||||
dev.max = max
|
||||
dev.lastBrightness = value
|
||||
dev.max = maxValue
|
||||
dev.lastBrightness = raw
|
||||
b.devices.Store(id, dev)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) getVCPFeature(fd int, vcp byte) (*ddcCapability, error) {
|
||||
for range 3 {
|
||||
dummy := make([]byte, 32)
|
||||
n, _ := syscall.Read(fd, dummy)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
// Based on ddcutil's i2c_check_open_bus_alive() (src/i2c/i2c_bus_core.c), sysfs side only. Known
|
||||
// devices are keyed by bus number, which the kernel hands to the next adapter once one goes away.
|
||||
func (b *DDCBackend) checkBusAlive(dev *ddcDevice) error {
|
||||
if isIgnorableI2CBus(dev.bus) || getI2CDeviceSysfsName(dev.bus) != dev.adapter {
|
||||
return errBusReused
|
||||
}
|
||||
connectors := drmConnectorsByBus()
|
||||
if ddcBusVerdictFor(dev.bus, connectors) == ddcBusSkip {
|
||||
return errNoMonitor
|
||||
}
|
||||
if ddcDisplayAsleep(dev.bus, connectors) {
|
||||
return errDisplayAsleep
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ddcRawFromPercent(percent, maxValue int) int {
|
||||
percent = min(max(percent, 0), 100)
|
||||
return int(math.Round(float64(percent*maxValue) / 100))
|
||||
}
|
||||
|
||||
func ddcPercentFromRaw(raw, maxValue int) int {
|
||||
if maxValue <= 0 {
|
||||
return 0
|
||||
}
|
||||
return min(int(math.Round(float64(raw*100)/float64(maxValue))), 100)
|
||||
}
|
||||
|
||||
func (b *DDCBackend) getVCPFeature(fd int, vcp byte) (*ddcCapability, error) {
|
||||
data := []byte{
|
||||
DDCCI_VCP_GET,
|
||||
vcp,
|
||||
@@ -347,25 +555,6 @@ func (b *DDCBackend) getVCPFeature(fd int, vcp byte) (*ddcCapability, error) {
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
pollFds := []unix.PollFd{
|
||||
{
|
||||
Fd: int32(fd),
|
||||
Events: unix.POLLIN,
|
||||
},
|
||||
}
|
||||
|
||||
pollTimeout := 200
|
||||
pollResult, err := unix.Poll(pollFds, pollTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("poll i2c: %w", err)
|
||||
}
|
||||
if pollResult == 0 {
|
||||
return nil, fmt.Errorf("poll timeout after %dms", pollTimeout)
|
||||
}
|
||||
if pollFds[0].Revents&unix.POLLIN == 0 {
|
||||
return nil, fmt.Errorf("poll returned but POLLIN not set")
|
||||
}
|
||||
|
||||
response := make([]byte, 12)
|
||||
n, err = syscall.Read(fd, response)
|
||||
if err != nil || n < 8 {
|
||||
@@ -520,4 +709,8 @@ func (b *DDCBackend) WaitPending() {
|
||||
}
|
||||
|
||||
func (b *DDCBackend) Close() {
|
||||
if b.stop == nil {
|
||||
return
|
||||
}
|
||||
b.closeOnce.Do(func() { close(b.stop) })
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -11,9 +14,12 @@ import (
|
||||
)
|
||||
|
||||
// isIgnorableI2CBus checks if an I2C bus should be skipped during DDC probing.
|
||||
// Based on ddcutil's sysfs_is_ignorable_i2c_device() (sysfs_base.c:1441)
|
||||
// Based on ddcutil's sysfs_is_ignorable_i2c_device() (src/sysfs/sysfs_simple.c)
|
||||
func isIgnorableI2CBus(busno int) bool {
|
||||
name := getI2CDeviceSysfsName(busno)
|
||||
if name == "DPMST" {
|
||||
return false
|
||||
}
|
||||
driver := getI2CSysfsDriver(busno)
|
||||
|
||||
if name != "" && isIgnorableI2CDeviceName(name, driver) {
|
||||
@@ -23,19 +29,20 @@ func isIgnorableI2CBus(busno int) bool {
|
||||
|
||||
// Only probe display adapters (0x03xxxx) and docking stations (0x0axxxx)
|
||||
class := getI2CDeviceSysfsClass(busno)
|
||||
if class != 0 {
|
||||
classHigh := class & 0xFFFF0000
|
||||
ignorable := (classHigh != 0x030000 && classHigh != 0x0A0000)
|
||||
if ignorable {
|
||||
log.Debugf("i2c-%d: ignoring class 0x%08x", busno, class)
|
||||
}
|
||||
return ignorable
|
||||
if class == 0 {
|
||||
// No PCI class says nothing about a platform adapter, but a real adapter always has a name.
|
||||
return name == ""
|
||||
}
|
||||
|
||||
return false
|
||||
classHigh := class & 0xFFFF0000
|
||||
ignorable := (classHigh != 0x030000 && classHigh != 0x0A0000)
|
||||
if ignorable {
|
||||
log.Debugf("i2c-%d: ignoring class 0x%08x", busno, class)
|
||||
}
|
||||
return ignorable
|
||||
}
|
||||
|
||||
// Based on ddcutil's ignorable_i2c_device_sysfs_name() (sysfs_base.c:1408)
|
||||
// Based on ddcutil's ignorable_i2c_device_sysfs_name() (src/sysfs/sysfs_simple.c)
|
||||
func isIgnorableI2CDeviceName(name, driver string) bool {
|
||||
ignorablePrefixes := []string{
|
||||
"SMBus",
|
||||
@@ -44,7 +51,8 @@ func isIgnorableI2CDeviceName(name, driver string) bool {
|
||||
"smu",
|
||||
"mac-io",
|
||||
"u4",
|
||||
"AMDGPU SMU", // AMD Navi2+ - probing hangs GPU
|
||||
"AMDGPU SMU", // AMD Navi2+ - probing hangs GPU
|
||||
"AMDGPU DM i2c OEM bus", // RGB controllers, not displays
|
||||
}
|
||||
|
||||
for _, prefix := range ignorablePrefixes {
|
||||
@@ -71,65 +79,194 @@ func getI2CDeviceSysfsName(busno int) string {
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// Based on ddcutil's get_i2c_device_sysfs_class() (sysfs_base.c:1380)
|
||||
// Based on ddcutil's get_i2c_device_sysfs_class() (src/sysfs/sysfs_simple.c)
|
||||
func getI2CDeviceSysfsClass(busno int) uint32 {
|
||||
classPath := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device/class", busno)
|
||||
data, err := os.ReadFile(classPath)
|
||||
if err != nil {
|
||||
classPath = fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device/device/device/class", busno)
|
||||
data, err = os.ReadFile(classPath)
|
||||
paths := []string{
|
||||
fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d", busno),
|
||||
fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device", busno),
|
||||
fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/i2c-dev/i2c-%d/device", busno, busno),
|
||||
}
|
||||
for _, path := range paths {
|
||||
adapter := findI2CAdapter(path)
|
||||
if adapter == "" {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(adapter, "class"))
|
||||
if err != nil {
|
||||
return 0
|
||||
continue
|
||||
}
|
||||
class, err := strconv.ParseUint(strings.TrimPrefix(strings.TrimSpace(string(data)), "0x"), 16, 32)
|
||||
if err == nil && class != 0 {
|
||||
return uint32(class)
|
||||
}
|
||||
}
|
||||
|
||||
classStr := strings.TrimSpace(string(data))
|
||||
classStr = strings.TrimPrefix(classStr, "0x")
|
||||
|
||||
class, err := strconv.ParseUint(classStr, 16, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint32(class)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Based on ddcutil's get_i2c_sysfs_driver_by_busno() (sysfs_base.c:1284)
|
||||
// Based on ddcutil's get_driver_for_busno() (src/sysfs/sysfs_simple.c)
|
||||
func getI2CSysfsDriver(busno int) string {
|
||||
devicePath := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d", busno)
|
||||
adapterPath, err := findI2CAdapter(devicePath)
|
||||
adapter := findI2CAdapter(fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d", busno))
|
||||
if adapter == "" {
|
||||
return ""
|
||||
}
|
||||
module, err := filepath.EvalSymlinks(filepath.Join(adapter, "driver", "module"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(module)
|
||||
}
|
||||
|
||||
driverLink := filepath.Join(adapterPath, "driver")
|
||||
target, err := os.Readlink(driverLink)
|
||||
// Based on ddcutil's sysfs_find_adapter() (src/sysfs/sysfs_simple.c): the nearest
|
||||
// ancestor under /sys/devices carrying a class attribute.
|
||||
func findI2CAdapter(path string) string {
|
||||
resolved, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Base(target)
|
||||
}
|
||||
|
||||
func findI2CAdapter(devicePath string) (string, error) {
|
||||
currentPath := devicePath
|
||||
|
||||
for range 10 {
|
||||
if _, err := os.Stat(filepath.Join(currentPath, "name")); err == nil {
|
||||
return currentPath, nil
|
||||
for strings.HasPrefix(resolved, "/sys/devices") {
|
||||
if info, err := os.Stat(filepath.Join(resolved, "class")); err == nil && info.Mode().IsRegular() {
|
||||
return resolved
|
||||
}
|
||||
|
||||
deviceLink := filepath.Join(currentPath, "device")
|
||||
target, err := os.Readlink(deviceLink)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(filepath.Dir(currentPath), target)
|
||||
}
|
||||
currentPath = filepath.Clean(target)
|
||||
resolved = filepath.Dir(resolved)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find adapter for %s", devicePath)
|
||||
// ddcutil's known_reliable_drivers (src/sysfs/sysfs_base.c): drivers that keep the DRM connector
|
||||
// edid, status and dpms attributes current.
|
||||
var sysfsReliableDrivers = []string{"i915", "xe", "amdgpu", "radeon", "nouveau"}
|
||||
|
||||
var (
|
||||
drmConnectorPattern = regexp.MustCompile(`^card[0-9]+-`)
|
||||
i2cBusPattern = regexp.MustCompile(`^i2c-([0-9]+)$`)
|
||||
)
|
||||
|
||||
type ddcBusVerdict int
|
||||
|
||||
const (
|
||||
ddcBusSkip ddcBusVerdict = iota
|
||||
ddcBusHasEDID
|
||||
ddcBusNeedsEDIDRead
|
||||
)
|
||||
|
||||
// Based on ddcutil's i2c_edid_exists() and its laptop panel exclusion (src/i2c/i2c_bus_core.c).
|
||||
func ddcBusVerdictFor(busno int, connectors map[int]string) ddcBusVerdict {
|
||||
name := getI2CDeviceSysfsName(busno)
|
||||
displayLink := name == "DisplayLink I2C Adapter"
|
||||
reliable := slices.Contains(sysfsReliableDrivers, getI2CSysfsDriver(busno))
|
||||
|
||||
connector, mapped := connectors[busno]
|
||||
if !mapped {
|
||||
if reliable && !displayLink && name != "DPMST" && len(connectors) > 0 {
|
||||
return ddcBusSkip
|
||||
}
|
||||
return ddcBusNeedsEDIDRead
|
||||
}
|
||||
if strings.Contains(connector, "-eDP-") || strings.Contains(connector, "-LVDS-") {
|
||||
return ddcBusSkip
|
||||
}
|
||||
if reliable || displayLink {
|
||||
if len(drmConnectorAttr(connector, "edid")) >= 128 {
|
||||
return ddcBusHasEDID
|
||||
}
|
||||
return ddcBusSkip
|
||||
}
|
||||
if strings.TrimSpace(drmConnectorAttr(connector, "status")) == "disconnected" {
|
||||
return ddcBusSkip
|
||||
}
|
||||
return ddcBusNeedsEDIDRead
|
||||
}
|
||||
|
||||
// Based on ddcutil's dpms_check_drm_asleep_by_businfo() (src/sysfs/sysfs_dpms.c).
|
||||
func ddcDisplayAsleep(busno int, connectors map[int]string) bool {
|
||||
connector, mapped := connectors[busno]
|
||||
if !mapped || !slices.Contains(sysfsReliableDrivers, getI2CSysfsDriver(busno)) {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(drmConnectorAttr(connector, "dpms")) != "On"
|
||||
}
|
||||
|
||||
// Based on ddcutil's get_connector_bus_numbers() (src/sysfs/sysfs_simple.c): a DP connector's own
|
||||
// i2c-N directory, any other connector's ddc/i2c-dev/i2c-N.
|
||||
func drmConnectorsByBus() map[int]string {
|
||||
connectors := map[int]string{}
|
||||
entries, err := os.ReadDir("/sys/class/drm")
|
||||
if err != nil {
|
||||
return connectors
|
||||
}
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if !drmConnectorPattern.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join("/sys/class/drm", name)
|
||||
if !strings.Contains(name, "-DP-") {
|
||||
dir = filepath.Join(dir, "ddc", "i2c-dev")
|
||||
}
|
||||
if busno, ok := i2cSubdirBus(dir); ok {
|
||||
connectors[busno] = name
|
||||
}
|
||||
}
|
||||
return connectors
|
||||
}
|
||||
|
||||
func i2cSubdirBus(dir string) (int, bool) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
for _, entry := range entries {
|
||||
match := i2cBusPattern.FindStringSubmatch(entry.Name())
|
||||
if match == nil {
|
||||
continue
|
||||
}
|
||||
if busno, err := strconv.Atoi(match[1]); err == nil {
|
||||
return busno, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func drmConnectorAttr(connector, attr string) string {
|
||||
data, err := os.ReadFile(filepath.Join("/sys/class/drm", connector, attr))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
var edidHeader = []byte{0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}
|
||||
|
||||
// Based on ddcutil's is_valid_raw_edid() (src/util/edid.c).
|
||||
func isValidEDID(edid []byte) bool {
|
||||
if len(edid) < 128 || !bytes.Equal(edid[:8], edidHeader) {
|
||||
return false
|
||||
}
|
||||
var sum byte
|
||||
for _, b := range edid[:128] {
|
||||
sum += b
|
||||
}
|
||||
return sum == 0
|
||||
}
|
||||
|
||||
// Based on ddcutil's is_laptop_parsed_edid() (src/util/edid.c).
|
||||
func isLaptopEDID(edid []byte) bool {
|
||||
return edidDescriptorText(edid, 0xfc) == "" && edidDescriptorText(edid, 0xff) == ""
|
||||
}
|
||||
|
||||
// Based on ddcutil's get_edid_descriptor_strings() (src/util/edid.c).
|
||||
func edidDescriptorText(edid []byte, tag byte) string {
|
||||
text := ""
|
||||
for i := range 4 {
|
||||
descriptor := edid[54+i*18 : 72+i*18]
|
||||
if descriptor[0] != 0 || descriptor[1] != 0 || descriptor[2] != 0 || descriptor[4] != 0 || descriptor[3] != tag {
|
||||
continue
|
||||
}
|
||||
raw := descriptor[5:18]
|
||||
if end := bytes.IndexByte(raw, 0x0a); end >= 0 {
|
||||
raw = raw[:end]
|
||||
}
|
||||
text = strings.TrimRight(string(raw), " \t\n\v\f\r")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -42,12 +42,12 @@ func (m *Manager) initLogind() {
|
||||
}
|
||||
|
||||
func (m *Manager) initDDC() {
|
||||
if os.Getenv("DMS_NO_DDC") != "" {
|
||||
ddc, err := newDDCBackend(m.updateState)
|
||||
switch {
|
||||
case errors.Is(err, errDDCDisabled):
|
||||
log.Info("DDC backend disabled via DMS_NO_DDC")
|
||||
return
|
||||
}
|
||||
ddc, err := NewDDCBackend()
|
||||
if err != nil {
|
||||
case err != nil:
|
||||
log.Debugf("Failed to initialize DDC backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@ type DDCBackend struct {
|
||||
debounceTimers map[string]*time.Timer
|
||||
debouncePending map[string]ddcPendingSet
|
||||
debounceWg sync.WaitGroup
|
||||
|
||||
ioMutex sync.Mutex
|
||||
onReread func()
|
||||
stop chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
type ddcPendingSet struct {
|
||||
@@ -108,6 +113,7 @@ type ddcDevice struct {
|
||||
addr int
|
||||
id string
|
||||
name string
|
||||
adapter string
|
||||
max int
|
||||
lastBrightness int
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
handlePinEntry(conn, req, m)
|
||||
case "clipboard.unpinEntry":
|
||||
handleUnpinEntry(conn, req, m)
|
||||
case "clipboard.editEntry":
|
||||
handleEditEntry(conn, req, m)
|
||||
case "clipboard.getPinnedEntries":
|
||||
handleGetPinnedEntries(conn, req, m)
|
||||
case "clipboard.getPinnedCount":
|
||||
@@ -422,3 +424,24 @@ func handleCopyFile(conn *models.Conn, req models.Request, m *Manager) {
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied"})
|
||||
}
|
||||
|
||||
func handleEditEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
text, err := params.String(req.Params, "text")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.EditEntry(uint64(id), text); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry updated"})
|
||||
}
|
||||
|
||||
@@ -795,6 +795,13 @@ func selectAltTextMimeType(mimes []string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func isTextMimeType(mime string) bool {
|
||||
if mime == "" || strings.HasPrefix(mime, "text/plain") {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(altTextMimeTypes, mime)
|
||||
}
|
||||
|
||||
func (m *Manager) isImageMimeType(mime string) bool {
|
||||
return strings.HasPrefix(mime, "image/")
|
||||
}
|
||||
@@ -1963,6 +1970,113 @@ func (m *Manager) UnpinEntry(id uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) EditEntry(id uint64, text string) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
data := []byte(text)
|
||||
mimeType := "text/plain;charset=utf-8"
|
||||
|
||||
if len(bytes.TrimSpace(data)) == 0 {
|
||||
return fmt.Errorf("cannot save empty entry")
|
||||
}
|
||||
|
||||
cfg := m.getConfig()
|
||||
if int64(len(data)) > cfg.MaxEntrySize {
|
||||
return fmt.Errorf("data too large")
|
||||
}
|
||||
|
||||
newHash := computeHash(data)
|
||||
preview := m.textPreview(data)
|
||||
|
||||
err := m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return fmt.Errorf("clipboard bucket missing")
|
||||
}
|
||||
|
||||
oldKey := itob(id)
|
||||
v := b.Get(oldKey)
|
||||
if v == nil {
|
||||
return errEntryNotFound
|
||||
}
|
||||
|
||||
existing, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.IsImage {
|
||||
return errors.New("cannot edit image entry")
|
||||
}
|
||||
|
||||
if !isTextMimeType(existing.MimeType) {
|
||||
return errors.New("cannot edit non-text entry")
|
||||
}
|
||||
|
||||
wasPinned := existing.Pinned
|
||||
|
||||
if err := b.Delete(oldKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.deduplicateInTx(b, newHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if wasPinned {
|
||||
c := b.Cursor()
|
||||
for k, val := c.First(); k != nil; k, val = c.Next() {
|
||||
if extractHash(val) == newHash {
|
||||
meta, err := decodeEntryMeta(val)
|
||||
if err == nil && meta.Pinned {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newID, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newEntry := Entry{
|
||||
ID: newID,
|
||||
Data: data,
|
||||
MimeType: mimeType,
|
||||
Preview: preview,
|
||||
Size: len(data),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: false,
|
||||
Hash: newHash,
|
||||
Pinned: wasPinned,
|
||||
}
|
||||
|
||||
encoded, err := encodeEntry(newEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.Put(itob(newID), encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.trimLengthInTx(b)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
if clipErr := m.SetClipboard(data, mimeType); clipErr != nil {
|
||||
log.Errorf("Failed to set clipboard selection: %v", clipErr)
|
||||
}
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) GetPinnedEntries() []Entry {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
|
||||
@@ -45,9 +45,15 @@ func newTestManagerWithDB(t *testing.T) *Manager {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
mockCtx := mocks_wlcontext.NewMockWaylandContext(t)
|
||||
mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) {
|
||||
fn()
|
||||
}).Maybe()
|
||||
|
||||
return &Manager{
|
||||
config: DefaultConfig(),
|
||||
db: db,
|
||||
wlCtx: mockCtx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,6 +584,175 @@ func TestCreateHistoryEntryFromPinned_KeepsLatestUnpinnedDuplicate(t *testing.T)
|
||||
assert.NotEqual(t, firstDuplicate.ID, latestDuplicate.ID)
|
||||
}
|
||||
|
||||
func TestEditEntry_UnpinnedEntry(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
id := storeTestEntry(t, m, "original unpinned")
|
||||
require.NoError(t, m.EditEntry(id, "edited unpinned"))
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
assert.Equal(t, "edited unpinned", history[0].Preview)
|
||||
assert.False(t, history[0].Pinned)
|
||||
assert.NotEqual(t, id, history[0].ID)
|
||||
|
||||
// Old entry should not exist
|
||||
oldEntry, err := m.GetEntry(id)
|
||||
assert.ErrorIs(t, err, errEntryNotFound)
|
||||
assert.Nil(t, oldEntry)
|
||||
}
|
||||
|
||||
func TestEditEntry_PinnedEntryRemainsPinned(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
id := storeTestEntry(t, m, "original pinned")
|
||||
require.NoError(t, m.PinEntry(id))
|
||||
assert.Equal(t, 1, m.GetPinnedCount())
|
||||
|
||||
require.NoError(t, m.EditEntry(id, "edited pinned"))
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
assert.Equal(t, "edited pinned", history[0].Preview)
|
||||
assert.True(t, history[0].Pinned)
|
||||
|
||||
pinnedEntries := m.GetPinnedEntries()
|
||||
require.Len(t, pinnedEntries, 1)
|
||||
assert.Equal(t, "edited pinned", pinnedEntries[0].Preview)
|
||||
assert.Equal(t, 1, m.GetPinnedCount())
|
||||
|
||||
// Old pinned entry should be deleted
|
||||
oldEntry, err := m.GetEntry(id)
|
||||
assert.ErrorIs(t, err, errEntryNotFound)
|
||||
assert.Nil(t, oldEntry)
|
||||
}
|
||||
|
||||
func TestEditEntry_NotFound(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
err := m.EditEntry(99999, "new text")
|
||||
assert.ErrorIs(t, err, errEntryNotFound)
|
||||
}
|
||||
|
||||
func TestEditEntry_ImageReturnsError(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
imgEntry := Entry{
|
||||
Data: []byte{0x89, 0x50, 0x4E, 0x47},
|
||||
MimeType: "image/png",
|
||||
Preview: "[[ image ]]",
|
||||
Size: 4,
|
||||
Timestamp: time.Now().Truncate(time.Second),
|
||||
IsImage: true,
|
||||
}
|
||||
require.NoError(t, m.storeEntry(imgEntry))
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
id := history[0].ID
|
||||
|
||||
err := m.EditEntry(id, "replacement text")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot edit image entry")
|
||||
}
|
||||
|
||||
func TestEditEntry_NonTextReturnsError(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
uriEntry := Entry{
|
||||
Data: []byte("file:///path/to/file\n"),
|
||||
MimeType: "text/uri-list",
|
||||
Preview: "file:///path/to/file",
|
||||
Size: 21,
|
||||
Timestamp: time.Now().Truncate(time.Second),
|
||||
IsImage: false,
|
||||
}
|
||||
require.NoError(t, m.storeEntry(uriEntry))
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
id := history[0].ID
|
||||
|
||||
err := m.EditEntry(id, "replacement text")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot edit non-text entry")
|
||||
}
|
||||
|
||||
func TestEditEntry_AltTextMimeTypesAllowed(t *testing.T) {
|
||||
for _, mime := range []string{"UTF8_STRING", "STRING", "TEXT", "text/plain;charset=utf-8", "text/plain"} {
|
||||
t.Run(mime, func(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
entry := Entry{
|
||||
Data: []byte("old text"),
|
||||
MimeType: mime,
|
||||
Preview: "old text",
|
||||
Size: 8,
|
||||
Timestamp: time.Now().Truncate(time.Second),
|
||||
IsImage: false,
|
||||
}
|
||||
require.NoError(t, m.storeEntry(entry))
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
id := history[0].ID
|
||||
|
||||
err := m.EditEntry(id, "replacement text")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditEntry_EmptyOrWhitespaceReturnsError(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
id := storeTestEntry(t, m, "keep me")
|
||||
|
||||
for _, badText := range []string{"", " ", "\t\n\r"} {
|
||||
err := m.EditEntry(id, badText)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot save empty entry")
|
||||
}
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
assert.Equal(t, "keep me", history[0].Preview)
|
||||
}
|
||||
|
||||
func TestEditEntry_DataTooLargeReturnsError(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
m.config.MaxEntrySize = 10
|
||||
id := storeTestEntry(t, m, "small")
|
||||
|
||||
err := m.EditEntry(id, "this text exceeds the 10-byte limit")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "data too large")
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
assert.Equal(t, "small", history[0].Preview)
|
||||
}
|
||||
|
||||
func TestHandleEditEntry_SuccessAndValidation(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
id := storeTestEntry(t, m, "before edit")
|
||||
|
||||
mc := newClipboardTestConn()
|
||||
conn := models.NewConn(mc)
|
||||
handleEditEntry(conn, models.Request{
|
||||
ID: 1,
|
||||
Params: map[string]any{
|
||||
"id": float64(id),
|
||||
"text": "after edit",
|
||||
},
|
||||
}, m)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Success)
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
assert.Equal(t, "after edit", history[0].Preview)
|
||||
}
|
||||
|
||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
||||
m := &Manager{
|
||||
subscribers: make(map[string]chan State),
|
||||
|
||||
@@ -126,7 +126,7 @@ func findKeyboards() ([]EvdevDevice, error) {
|
||||
|
||||
var keyboards []EvdevDevice
|
||||
for _, path := range matches {
|
||||
device, err := evdev.Open(path)
|
||||
device, err := evdev.OpenWithFlags(path, os.O_RDONLY)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func (m *Manager) watchForNewKeyboards() {
|
||||
continue
|
||||
}
|
||||
|
||||
device, err := evdev.Open(event.Name)
|
||||
device, err := evdev.OpenWithFlags(event.Name, os.O_RDONLY)
|
||||
if err != nil {
|
||||
m.devicesMutex.Unlock()
|
||||
continue
|
||||
|
||||
@@ -154,23 +154,7 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]
|
||||
}
|
||||
if _, hasDevice := interfaces[iwdDeviceInterface]; hasDevice {
|
||||
b.devicePath = path
|
||||
|
||||
if devProps, ok := interfaces[iwdDeviceInterface]; ok {
|
||||
if nameVar, ok := devProps["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiDevice = name
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
if poweredVar, ok := devProps["Powered"]; ok {
|
||||
if powered, ok := poweredVar.Value().(bool); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = powered
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
b.applyDeviceProps(interfaces[iwdDeviceInterface])
|
||||
}
|
||||
if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter {
|
||||
b.adapterPath = path
|
||||
@@ -193,6 +177,47 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IWDBackend) applyDeviceProps(props map[string]dbus.Variant) {
|
||||
if nameVar, ok := props["Name"]; ok {
|
||||
if name, ok := nameVar.Value().(string); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiDevice = name
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
if poweredVar, ok := props["Powered"]; ok {
|
||||
if powered, ok := poweredVar.Value().(bool); ok {
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = powered
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setDevice records a device that appeared at a new path, which is what
|
||||
// happens when iwd restarts and recreates wlan0. It returns the path it
|
||||
// replaced so the caller can move the signal match over.
|
||||
func (b *IWDBackend) setDevice(path dbus.ObjectPath, props map[string]dbus.Variant) (prev dbus.ObjectPath, changed bool) {
|
||||
if path == "" || path == b.devicePath {
|
||||
return "", false
|
||||
}
|
||||
prev = b.devicePath
|
||||
b.devicePath = path
|
||||
b.applyDeviceProps(props)
|
||||
return prev, true
|
||||
}
|
||||
|
||||
func (b *IWDBackend) clearDevice(path dbus.ObjectPath) bool {
|
||||
if path == "" || path != b.devicePath {
|
||||
return false
|
||||
}
|
||||
b.devicePath = ""
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiEnabled = false
|
||||
b.stateMutex.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetCurrentState() (*BackendState, error) {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
|
||||
@@ -26,12 +26,7 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.conn.Signal(sigChan)
|
||||
|
||||
if b.devicePath != "" {
|
||||
err := b.conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(b.devicePath),
|
||||
dbus.WithMatchInterface(dbusPropertiesInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
if err != nil {
|
||||
if err := b.addDeviceSignalMatch(b.devicePath); err != nil {
|
||||
return fmt.Errorf("failed to add device signal match: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -86,6 +81,48 @@ func (b *IWDBackend) addStationSignalMatch(path dbus.ObjectPath) error {
|
||||
)
|
||||
}
|
||||
|
||||
func (b *IWDBackend) addDeviceSignalMatch(path dbus.ObjectPath) error {
|
||||
return b.conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(path),
|
||||
dbus.WithMatchInterface(dbusPropertiesInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
}
|
||||
|
||||
func (b *IWDBackend) removeDeviceSignalMatch(path dbus.ObjectPath) {
|
||||
err := b.conn.RemoveMatchSignal(
|
||||
dbus.WithMatchObjectPath(path),
|
||||
dbus.WithMatchInterface(dbusPropertiesInterface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to remove iwd device signal match for %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) handleDeviceAdded(path dbus.ObjectPath, props map[string]dbus.Variant) bool {
|
||||
prev, changed := b.setDevice(path, props)
|
||||
if !changed {
|
||||
return false
|
||||
}
|
||||
|
||||
if prev != "" {
|
||||
b.removeDeviceSignalMatch(prev)
|
||||
}
|
||||
if err := b.addDeviceSignalMatch(path); err != nil {
|
||||
log.Warnf("Failed to add iwd device signal match for %s: %v", path, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *IWDBackend) handleDeviceRemoved(path dbus.ObjectPath) bool {
|
||||
if !b.clearDevice(path) {
|
||||
return false
|
||||
}
|
||||
b.removeDeviceSignalMatch(path)
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *IWDBackend) handleStationAdded(path dbus.ObjectPath) bool {
|
||||
if path == "" || path == b.stationPath {
|
||||
return false
|
||||
@@ -139,6 +176,12 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
||||
if len(sig.Body) >= 2 {
|
||||
path, _ := sig.Body[0].(dbus.ObjectPath)
|
||||
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
|
||||
// Device before Station: handleStationAdded reads Powered from devicePath.
|
||||
if props, ok := interfaces[iwdDeviceInterface]; ok {
|
||||
if b.handleDeviceAdded(path, props) && b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
if _, ok := interfaces[iwdStationInterface]; ok {
|
||||
if b.handleStationAdded(path) && b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
@@ -159,17 +202,19 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
||||
path, _ := sig.Body[0].(dbus.ObjectPath)
|
||||
if interfaces, ok := sig.Body[1].([]string); ok {
|
||||
for _, iface := range interfaces {
|
||||
if iface == iwdStationInterface {
|
||||
switch iface {
|
||||
case iwdDeviceInterface:
|
||||
if b.handleDeviceRemoved(path) && b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
case iwdStationInterface:
|
||||
if b.handleStationRemoved(path) && b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
break
|
||||
}
|
||||
if iface == iwdKnownNetworkInterface {
|
||||
case iwdKnownNetworkInterface:
|
||||
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,3 +460,71 @@ func TestConnectAttempt_DoubleFinalization(t *testing.T) {
|
||||
assert.Equal(t, "bad-credentials", backend.state.LastError)
|
||||
backend.stateMutex.RUnlock()
|
||||
}
|
||||
|
||||
func TestIWDBackend_SetDevice_FollowsRecreatedDevice(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
|
||||
"/net/connman/iwd/0/3": {
|
||||
iwdDeviceInterface: {"Name": dbus.MakeVariant("wlan0"), "Powered": dbus.MakeVariant(true)},
|
||||
iwdStationInterface: {},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// iwd restarts: the old objects go away and wlan0 comes back with a new ifindex.
|
||||
assert.True(t, backend.handleStationRemoved("/net/connman/iwd/0/3"))
|
||||
assert.True(t, backend.clearDevice("/net/connman/iwd/0/3"))
|
||||
|
||||
prev, changed := backend.setDevice("/net/connman/iwd/0/7", map[string]dbus.Variant{
|
||||
"Name": dbus.MakeVariant("wlan0"),
|
||||
"Powered": dbus.MakeVariant(true),
|
||||
})
|
||||
|
||||
assert.True(t, changed)
|
||||
assert.Empty(t, prev)
|
||||
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/7"), backend.devicePath)
|
||||
|
||||
enabled, _ := backend.GetWiFiEnabled()
|
||||
assert.True(t, enabled, "Wi-Fi must be enabled again once the recreated device reports Powered")
|
||||
}
|
||||
|
||||
func TestIWDBackend_SetDevice_ReplacesPathWithoutRemovalSignal(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
backend.devicePath = "/net/connman/iwd/0/3"
|
||||
|
||||
prev, changed := backend.setDevice("/net/connman/iwd/0/7", map[string]dbus.Variant{
|
||||
"Powered": dbus.MakeVariant(true),
|
||||
})
|
||||
|
||||
assert.True(t, changed)
|
||||
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/3"), prev)
|
||||
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/7"), backend.devicePath)
|
||||
}
|
||||
|
||||
func TestIWDBackend_SetDevice_SamePathIsNoop(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
backend.devicePath = "/net/connman/iwd/0/3"
|
||||
|
||||
_, changed := backend.setDevice("/net/connman/iwd/0/3", map[string]dbus.Variant{
|
||||
"Powered": dbus.MakeVariant(false),
|
||||
})
|
||||
assert.False(t, changed)
|
||||
|
||||
_, changed = backend.setDevice("", nil)
|
||||
assert.False(t, changed)
|
||||
}
|
||||
|
||||
func TestIWDBackend_ClearDevice(t *testing.T) {
|
||||
backend, _ := NewIWDBackend()
|
||||
backend.devicePath = "/net/connman/iwd/0/3"
|
||||
|
||||
assert.False(t, backend.clearDevice("/net/connman/iwd/0/9"), "unrelated path must be ignored")
|
||||
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/3"), backend.devicePath)
|
||||
|
||||
assert.True(t, backend.clearDevice("/net/connman/iwd/0/3"))
|
||||
assert.Empty(t, backend.devicePath)
|
||||
|
||||
enabled, _ := backend.GetWiFiEnabled()
|
||||
assert.False(t, enabled)
|
||||
assert.ErrorContains(t, backend.SetWiFiEnabled(true), "no WiFi device available")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -30,12 +31,54 @@ func (l *linkInfo) isWired() bool {
|
||||
if looksVirtual(l.name) {
|
||||
return false
|
||||
}
|
||||
if l.linkType == "bridge" {
|
||||
return hasPhysicalMember(l.name)
|
||||
}
|
||||
if l.linkType != "" {
|
||||
return l.linkType == "ether"
|
||||
return isWiredLinkType(l.linkType)
|
||||
}
|
||||
return !strings.HasPrefix(l.name, "wlan") && !strings.HasPrefix(l.name, "wlp")
|
||||
}
|
||||
|
||||
// isWiredLinkType reports whether a networkd link Type can carry the wired
|
||||
// uplink. Link aggregates and bridges hold the IP configuration while the
|
||||
// enslaved member NIC stays in the "enslaved" state, same as #1581 for
|
||||
// NetworkManager. Bridges are further checked in isWired.
|
||||
func isWiredLinkType(t string) bool {
|
||||
switch t {
|
||||
case "ether", "bridge", "bond", "team", "vlan":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// bridgeMembers lists the interfaces enslaved to a bridge. It is a variable so
|
||||
// tests can stub the sysfs lookup.
|
||||
var bridgeMembers = func(bridge string) []string {
|
||||
entries, err := os.ReadDir("/sys/class/net/" + bridge + "/brif")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// hasPhysicalMember reports whether a bridge has at least one member that is
|
||||
// not virtual (veth, tap, ...). This tells a host bridge carrying the uplink
|
||||
// apart from container/VM bridges (lxc, waydroid, ...) that only hold virtual
|
||||
// members.
|
||||
func hasPhysicalMember(bridge string) bool {
|
||||
for _, m := range bridgeMembers(bridge) {
|
||||
if !looksVirtual(m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *linkInfo) isWireless() bool {
|
||||
if looksVirtual(l.name) {
|
||||
return false
|
||||
|
||||
@@ -166,6 +166,15 @@ func TestLinkInfo_Classify(t *testing.T) {
|
||||
{"veth ether excluded", "veth1234", "ether", false, false},
|
||||
{"podman bridge ether excluded", "podman3", "ether", false, false},
|
||||
{"docker bridge ether excluded", "docker0", "ether", false, false},
|
||||
// The IP lives on the aggregate while the member NIC is "enslaved" (#3463).
|
||||
{"bridge type", "br0", "bridge", true, false},
|
||||
{"bond type", "bond0", "bond", true, false},
|
||||
{"team type", "team0", "team", true, false},
|
||||
{"vlan type", "vlan10", "vlan", true, false},
|
||||
{"docker user bridge excluded", "br-1a2b3c", "bridge", false, false},
|
||||
{"libvirt bridge excluded", "virbr0", "bridge", false, false},
|
||||
{"bridge with only virtual members excluded", "lxcbr0", "bridge", false, false},
|
||||
{"bridge without members excluded", "waydroid0", "bridge", false, false},
|
||||
// Fallback path: linkType unavailable, name-prefix heuristic applies.
|
||||
{"fallback enp wired", "enp141s0", "", true, false},
|
||||
{"fallback wlan wireless", "wlan0", "", false, true},
|
||||
@@ -174,6 +183,15 @@ func TestLinkInfo_Classify(t *testing.T) {
|
||||
{"fallback docker skipped", "docker0", "", false, false},
|
||||
{"fallback tun skipped", "tun0", "", false, false},
|
||||
}
|
||||
members := map[string][]string{
|
||||
"br0": {"enp42s0", "vnet0"},
|
||||
"lxcbr0": {"vethAbCd12"},
|
||||
"waydroid0": nil,
|
||||
}
|
||||
orig := bridgeMembers
|
||||
bridgeMembers = func(b string) []string { return members[b] }
|
||||
t.Cleanup(func() { bridgeMembers = orig })
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := &linkInfo{name: tc.ifname, linkType: tc.linkType}
|
||||
@@ -254,7 +272,7 @@ func TestLooksVirtual(t *testing.T) {
|
||||
for _, n := range virtual {
|
||||
assert.True(t, looksVirtual(n), "%s should look virtual", n)
|
||||
}
|
||||
real := []string{"enp141s0", "eno1", "wlan0", "wlp3s0", "wifi", "dock", "nebula.homelab", "wg0"}
|
||||
real := []string{"enp141s0", "eno1", "wlan0", "wlp3s0", "wifi", "dock", "nebula.homelab", "wg0", "br0", "bond0", "team0", "vlan10"}
|
||||
for _, n := range real {
|
||||
assert.False(t, looksVirtual(n), "%s should not look virtual", n)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ const (
|
||||
|
||||
metricPreferred = int64(100)
|
||||
metricNonPreferred = int64(300)
|
||||
metricDefault = int64(100)
|
||||
// -1 lets NetworkManager pick the metric by device type (nm-settings ipv4.route-metric)
|
||||
metricDefault = int64(-1)
|
||||
)
|
||||
|
||||
func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error {
|
||||
@@ -46,7 +47,7 @@ func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error {
|
||||
case PreferenceCellular:
|
||||
return m.prioritizeCellular()
|
||||
case PreferenceAuto:
|
||||
return m.balancePriorities()
|
||||
return m.restoreDefaultPriorities()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -112,7 +113,7 @@ func (m *Manager) prioritizeCellular() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) balancePriorities() error {
|
||||
func (m *Manager) restoreDefaultPriorities() error {
|
||||
if err := m.setConnectionPriority("802-3-ethernet", priorityDefault, metricDefault); err != nil {
|
||||
log.Warnf("Failed to reset Ethernet priority: %v", err)
|
||||
}
|
||||
@@ -242,16 +243,20 @@ func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int
|
||||
}
|
||||
|
||||
func priorityMatches(variant dbus.Variant, expected int64) bool {
|
||||
value, ok := variantInt64(variant)
|
||||
return ok && value == expected
|
||||
return settingInt64(variant, int64(priorityDefault)) == expected
|
||||
}
|
||||
|
||||
func routeMetricMatches(section map[string]dbus.Variant, expected int64) bool {
|
||||
if section == nil {
|
||||
return false
|
||||
return settingInt64(section["route-metric"], metricDefault) == expected
|
||||
}
|
||||
|
||||
// GetSettings omits properties at their default, so a missing key is the default value
|
||||
func settingInt64(variant dbus.Variant, def int64) int64 {
|
||||
value, ok := variantInt64(variant)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
value, ok := variantInt64(section["route-metric"])
|
||||
return ok && value == expected
|
||||
return value
|
||||
}
|
||||
|
||||
func variantInt64(variant dbus.Variant) (int64, bool) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -45,7 +46,10 @@ func TestManager_GetConnectionPreference(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Full testing of priority operations would require mocking NetworkManager
|
||||
// D-Bus interfaces. The tests above cover the basic logic and error handling.
|
||||
// Integration tests would be needed for complete coverage of network connection
|
||||
// priority updates and reactivation.
|
||||
func TestProfileAtDefaultsMatchesAutoTargets(t *testing.T) {
|
||||
assert.True(t, priorityMatches(dbus.Variant{}, int64(priorityDefault)))
|
||||
assert.True(t, routeMetricMatches(nil, metricDefault))
|
||||
assert.True(t, routeMetricMatches(map[string]dbus.Variant{"method": dbus.MakeVariant("auto")}, metricDefault))
|
||||
assert.False(t, routeMetricMatches(map[string]dbus.Variant{"route-metric": dbus.MakeVariant(int64(100))}, metricDefault))
|
||||
assert.False(t, priorityMatches(dbus.MakeVariant(int32(100)), int64(priorityDefault)))
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func convertStatus(status *ipnstate.Status) *TailscaleState {
|
||||
|
||||
if status.Self != nil {
|
||||
state.Self = convertPeerStatus(status.Self, users)
|
||||
state.Self.Online = true
|
||||
}
|
||||
|
||||
if len(status.Peer) > 0 {
|
||||
|
||||
@@ -95,7 +95,7 @@ func newManager(client tailscaleClient) *Manager {
|
||||
func (m *Manager) watchLoop(ctx context.Context) {
|
||||
defer m.watchWG.Done()
|
||||
|
||||
mask := ipn.NotifyInitialState | ipn.NotifyInitialNetMap | ipn.NotifyRateLimit
|
||||
mask := ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyRateLimit
|
||||
backoff := time.Second
|
||||
unreachableSent := false
|
||||
|
||||
@@ -122,8 +122,13 @@ func (m *Manager) watchLoop(ctx context.Context) {
|
||||
log.Info("[Tailscale] Connected to IPN bus")
|
||||
m.markAvailable()
|
||||
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
for {
|
||||
notify, err := watcher.Next()
|
||||
_, err := watcher.Next()
|
||||
if err != nil {
|
||||
log.Warnf("[Tailscale] IPN bus error: %v", err)
|
||||
break
|
||||
@@ -131,9 +136,6 @@ func (m *Manager) watchLoop(ctx context.Context) {
|
||||
|
||||
backoff = time.Second
|
||||
|
||||
if notify.State == nil && notify.NetMap == nil { //nolint:staticcheck // NetMap is deprecated upstream but still the only activity signal on some platforms
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
|
||||
@@ -418,3 +418,55 @@ func TestManager_Actions_PropagateError(t *testing.T) {
|
||||
assert.Error(t, m.SetExitNode("nABC123"))
|
||||
assert.Error(t, m.SetAllowLANAccess(true))
|
||||
}
|
||||
|
||||
type dynamicWatcher struct {
|
||||
ctx context.Context
|
||||
ch chan ipn.Notify
|
||||
}
|
||||
|
||||
func (w *dynamicWatcher) Next() (ipn.Notify, error) {
|
||||
select {
|
||||
case n, ok := <-w.ch:
|
||||
if !ok {
|
||||
return ipn.Notify{}, fmt.Errorf("watcher closed")
|
||||
}
|
||||
return n, nil
|
||||
case <-w.ctx.Done():
|
||||
return ipn.Notify{}, w.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *dynamicWatcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWatchLoop_NotifyWithoutStateOrNetMap(t *testing.T) {
|
||||
var statusCalls atomic.Int32
|
||||
notifyCh := make(chan ipn.Notify, 1)
|
||||
|
||||
client := &mockClient{
|
||||
watchFn: func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
|
||||
return &dynamicWatcher{ctx: ctx, ch: notifyCh}, nil
|
||||
},
|
||||
statusFn: func(ctx context.Context) (*ipnstate.Status, error) {
|
||||
statusCalls.Add(1)
|
||||
return runningStatus(), nil
|
||||
},
|
||||
}
|
||||
|
||||
m := newManager(client)
|
||||
defer m.Close()
|
||||
|
||||
// Wait for the initial connect-time status call to complete.
|
||||
require.Eventually(t, func() bool {
|
||||
return statusCalls.Load() == 1
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
|
||||
// Send an event with State == nil and NetMap == nil.
|
||||
notifyCh <- ipn.Notify{Engine: &ipn.EngineStatus{}}
|
||||
|
||||
// Verify that the notification without State or NetMap triggers a second status call.
|
||||
require.Eventually(t, func() bool {
|
||||
return statusCalls.Load() >= 2
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -277,7 +277,8 @@ const (
|
||||
|
||||
// ipp defaults
|
||||
const (
|
||||
CharsetLanguage = "en-US"
|
||||
// cups 2.4.20+ validates this case-sensitively (lowercase only), and libcups itself sends lowercase
|
||||
CharsetLanguage = "en-us"
|
||||
Charset = "utf-8"
|
||||
ProtocolVersionMajor = int8(2)
|
||||
ProtocolVersionMinor = int8(0)
|
||||
|
||||
+1
-1
Submodule dank-qml-common updated: 26396ce432...1ae722232d
Generated
+4
-3
@@ -3,15 +3,16 @@
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1788376015,
|
||||
"narHash": "sha256-/tT8rTznLADndTwLBzdgzzgfJH1ZhYQibYo5v/F8U+U=",
|
||||
"lastModified": 1791392812,
|
||||
"narHash": "sha256-id6Y5XNO5K3KfXSOsEYVUwiq1ygYGNX5LeKSgHM3GR8=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "26396ce432d6c71c3f5367438f96f4a8d667e160",
|
||||
"rev": "1ae722232da7110d26cd2c355b6615e078e94197",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "AvengeMedia",
|
||||
"ref": "stable-1.6",
|
||||
"repo": "dank-qml-common",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
flake = false;
|
||||
};
|
||||
dank-qml-common = {
|
||||
url = "github:AvengeMedia/dank-qml-common";
|
||||
url = "github:AvengeMedia/dank-qml-common/stable-1.6";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
@@ -111,7 +111,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-nswS/ygp0oYMXp6YHbbIxBha4xvzjKAxcAS5yOpbslg=";
|
||||
vendorHash = "sha256-ejDi+TzstUtxtWpcl5yYw4UZuTnZraOsIdJuXPKxJ6s=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SessionData")
|
||||
|
||||
readonly property int sessionConfigVersion: 4
|
||||
readonly property int sessionConfigVersion: 5
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
|
||||
@@ -1988,8 +1988,11 @@ Singleton {
|
||||
function saveSettings() {
|
||||
if (_loading || _parseError || !_hasLoaded)
|
||||
return;
|
||||
const json = JSON.stringify(Store.toJson(root), null, 2);
|
||||
if (json === settingsFile.text())
|
||||
return;
|
||||
_selfWrite = true;
|
||||
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
|
||||
settingsFile.setText(json);
|
||||
if (_isReadOnly)
|
||||
_checkSettingsWritable();
|
||||
}
|
||||
@@ -3225,10 +3228,11 @@ Singleton {
|
||||
if (!app && !desktop)
|
||||
return -1;
|
||||
return rules.findIndex(rule => {
|
||||
if (!predicate(rule))
|
||||
if (!predicate(rule) || (rule.matchType || "contains").toString().toLowerCase() !== "exact")
|
||||
return false;
|
||||
const pattern = (rule.pattern || "").toString().toLowerCase();
|
||||
return pattern !== "" && (pattern === app || pattern === desktop);
|
||||
const value = !rule.field || rule.field === "appName" ? app : rule.field === "desktopEntry" ? desktop : "";
|
||||
return pattern !== "" && pattern === value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3246,20 +3250,31 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function _removeAppRule(appName, desktopEntry, predicate) {
|
||||
function _hasNoAction(rule) {
|
||||
return (rule.action || "default").toString().toLowerCase() === "default";
|
||||
}
|
||||
|
||||
// Edits the first enabled matching rule and drops it once it no longer does anything.
|
||||
function _updateAppRule(appName, desktopEntry, predicate, changes) {
|
||||
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||
const index = _appRuleIndex(rules, appName, desktopEntry, predicate);
|
||||
const index = _appRuleIndex(rules, appName, desktopEntry, rule => rule.enabled !== false && predicate(rule));
|
||||
if (index === -1)
|
||||
return;
|
||||
rules.splice(index, 1);
|
||||
return false;
|
||||
const rule = Object.assign(rules[index], changes);
|
||||
if (_hasNoAction(rule) && (rule.urgency || "default").toString().toLowerCase() === "default" && !_isDndBypassRule(rule))
|
||||
rules.splice(index, 1);
|
||||
notificationRules = rules;
|
||||
saveSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
function addMuteRuleForApp(appName, desktopEntry) {
|
||||
_addAppRule(appName, desktopEntry, {
|
||||
action: "mute"
|
||||
});
|
||||
if (!_updateAppRule(appName, desktopEntry, _hasNoAction, {
|
||||
action: "mute"
|
||||
}))
|
||||
_addAppRule(appName, desktopEntry, {
|
||||
action: "mute"
|
||||
});
|
||||
}
|
||||
|
||||
function isAppMuted(appName, desktopEntry) {
|
||||
@@ -3267,7 +3282,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function removeMuteRuleForApp(appName, desktopEntry) {
|
||||
_removeAppRule(appName, desktopEntry, _isMuteRule);
|
||||
_updateAppRule(appName, desktopEntry, _isMuteRule, {
|
||||
action: "default"
|
||||
});
|
||||
}
|
||||
|
||||
function isAppDndBypassed(appName, desktopEntry) {
|
||||
@@ -3276,14 +3293,19 @@ Singleton {
|
||||
|
||||
function setAppDndBypass(appName, desktopEntry, enabled) {
|
||||
if (!enabled) {
|
||||
_removeAppRule(appName, desktopEntry, _isDndBypassRule);
|
||||
_updateAppRule(appName, desktopEntry, _isDndBypassRule, {
|
||||
bypassDnd: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isAppDndBypassed(appName, desktopEntry))
|
||||
return;
|
||||
_addAppRule(appName, desktopEntry, {
|
||||
bypassDnd: true
|
||||
});
|
||||
if (!_updateAppRule(appName, desktopEntry, () => true, {
|
||||
bypassDnd: true
|
||||
}))
|
||||
_addAppRule(appName, desktopEntry, {
|
||||
bypassDnd: true
|
||||
});
|
||||
}
|
||||
|
||||
function updateNotificationRule(index, ruleData) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
.pragma library
|
||||
|
||||
// Qt Wayland sends scroll phases only for the finger axis source; pixelDelta also comes with wheels from Qt 6.12 (qtbase d19bd66b4)
|
||||
const Kind = Object.freeze({
|
||||
none: "none",
|
||||
touchpad: "touchpad",
|
||||
wheel: "wheel",
|
||||
highResWheel: "highResWheel"
|
||||
});
|
||||
|
||||
function isTouchpad(wheel) {
|
||||
if (wheel.phase === Qt.NoScrollPhase)
|
||||
return false;
|
||||
return wheel.pixelDelta.x !== 0 || wheel.pixelDelta.y !== 0;
|
||||
}
|
||||
|
||||
function verticalKind(wheel) {
|
||||
if (isTouchpad(wheel))
|
||||
return Kind.touchpad;
|
||||
const angle = wheel.angleDelta.y;
|
||||
if (angle === 0)
|
||||
return Kind.none;
|
||||
return angle % 120 === 0 ? Kind.wheel : Kind.highResWheel;
|
||||
}
|
||||
|
||||
function anyAxisDelta(wheel) {
|
||||
const point = isTouchpad(wheel) ? wheel.pixelDelta : wheel.angleDelta;
|
||||
return point.x || point.y;
|
||||
}
|
||||
@@ -83,5 +83,29 @@ function migrateToVersion(obj, targetVersion, settingsData) {
|
||||
session.configVersion = 4;
|
||||
}
|
||||
|
||||
if (currentVersion < 5) {
|
||||
console.info("SessionData: Migrating session to version 5");
|
||||
console.info("SessionData: Tray keys now use a stable id (or id::instance) instead of id::tooltipTitle");
|
||||
|
||||
function stripTrayKeys(list) {
|
||||
if (!list || list.constructor !== Array) return list;
|
||||
var out = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var raw = list[i];
|
||||
if (typeof raw !== "string") continue;
|
||||
var key = raw.includes("::") ? raw.split("::")[0] : raw;
|
||||
var hasDup = false;
|
||||
for (var j = 0; j < out.length; j++) {
|
||||
if (out[j] === key) { hasDup = true; break; }
|
||||
}
|
||||
if (key && !hasDup) out.push(key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (session.hiddenTrayIds !== undefined) session.hiddenTrayIds = stripTrayKeys(session.hiddenTrayIds);
|
||||
if (session.trayItemOrder !== undefined) session.trayItemOrder = stripTrayKeys(session.trayItemOrder);
|
||||
session.configVersion = 5;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -398,8 +398,8 @@ Item {
|
||||
|
||||
IpcHandler {
|
||||
function getFocusedScreenName() {
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
return Hyprland.focusedWorkspace.monitor.name;
|
||||
if (CompositorService.isHyprland && Hyprland.focusedMonitor) {
|
||||
return Hyprland.focusedMonitor.name;
|
||||
}
|
||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
return NiriService.currentOutput;
|
||||
|
||||
@@ -28,7 +28,7 @@ Item {
|
||||
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
||||
|
||||
readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null
|
||||
readonly property bool canEditEntry: !!entry && !(entry.isImage ?? false)
|
||||
readonly property bool canEditEntry: ClipboardService.canEditEntry(entry)
|
||||
readonly property bool hasTextAlternative: !!entry && (entry.altMimeType ?? "") !== ""
|
||||
readonly property string pinText: entry?.pinned || hasPinnedDuplicate ? I18n.tr("Unpin") : I18n.tr("Pin")
|
||||
readonly property string pinIcon: entry?.pinned || hasPinnedDuplicate ? "keep_off" : "push_pin"
|
||||
|
||||
@@ -14,6 +14,20 @@ Item {
|
||||
|
||||
property var entry: null
|
||||
property string editorText: ""
|
||||
property bool textLoaded: false
|
||||
property bool loadFailed: false
|
||||
|
||||
Timer {
|
||||
id: loadTimeoutTimer
|
||||
interval: 5000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.textLoaded) {
|
||||
root.loadFailed = true;
|
||||
ToastService.showError(I18n.tr("Failed to load clipboard entry", "clipboard editor: fetching the entry's full text failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function releaseTextInputFocus() {
|
||||
if (editField) {
|
||||
@@ -68,35 +82,22 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function setEntry(newEntry) {
|
||||
entry = newEntry;
|
||||
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
||||
if (editField) {
|
||||
editField.text = editorText;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (editField) {
|
||||
editField.forceActiveFocus();
|
||||
editField.cursorPosition = editField.text.length;
|
||||
}
|
||||
});
|
||||
|
||||
if (!newEntry || newEntry.isImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestedId = newEntry.id;
|
||||
function fetchEntry(requestedId) {
|
||||
loadFailed = false;
|
||||
loadTimeoutTimer.restart();
|
||||
DMSService.sendRequest("clipboard.getEntry", {
|
||||
"id": requestedId
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
loadTimeoutTimer.stop();
|
||||
if (!root.entry || root.entry.id !== requestedId) {
|
||||
return;
|
||||
}
|
||||
if (!response.result) {
|
||||
ClipboardService.refresh();
|
||||
if (response.error || !response.result) {
|
||||
root.loadFailed = true;
|
||||
ToastService.showError(I18n.tr("Failed to load clipboard entry", "clipboard editor: fetching the entry's full text failed"));
|
||||
if (!response.result) {
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const result = response.result;
|
||||
@@ -108,8 +109,12 @@ Item {
|
||||
}
|
||||
|
||||
if (!fullText || fullText.length === 0) {
|
||||
root.loadFailed = true;
|
||||
ToastService.showError(I18n.tr("Failed to load clipboard entry", "clipboard editor: fetching the entry's full text failed"));
|
||||
return;
|
||||
}
|
||||
root.loadFailed = false;
|
||||
root.textLoaded = true;
|
||||
root.editorText = fullText;
|
||||
if (editField) {
|
||||
if (fullText.length > 50000) {
|
||||
@@ -127,15 +132,47 @@ Item {
|
||||
});
|
||||
}
|
||||
|
||||
function setEntry(newEntry) {
|
||||
entry = newEntry;
|
||||
loadFailed = false;
|
||||
const hasFullText = typeof newEntry?.text === "string";
|
||||
textLoaded = !newEntry || !ClipboardService.canEditEntry(newEntry) || !(newEntry.id > 0) || hasFullText;
|
||||
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
||||
if (editField) {
|
||||
editField.text = editorText;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (editField) {
|
||||
editField.forceActiveFocus();
|
||||
editField.cursorPosition = editField.text.length;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasFullText || !newEntry || !ClipboardService.canEditEntry(newEntry) || !(newEntry.id > 0)) {
|
||||
loadTimeoutTimer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
fetchEntry(newEntry.id);
|
||||
}
|
||||
|
||||
function saveEntry(action) {
|
||||
const saveAction = action ?? "history";
|
||||
DMSService.sendRequest("clipboard.copy", {
|
||||
"text": root.editorText
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
||||
return;
|
||||
const entryId = root.entry?.id ?? 0;
|
||||
|
||||
if (entryId > 0 && !root.textLoaded) {
|
||||
if (root.loadFailed) {
|
||||
ToastService.showError(I18n.tr("Failed to load clipboard entry", "clipboard editor: fetching the entry's full text failed"));
|
||||
root.fetchEntry(entryId);
|
||||
} else {
|
||||
ToastService.showWarning(I18n.tr("Loading full text, please wait...", "clipboard editor: save blocked while the entry's full text is still being fetched"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
loadTimeoutTimer.stop();
|
||||
|
||||
const onComplete = function () {
|
||||
if (saveAction === "history") {
|
||||
modal.mode = "history";
|
||||
Qt.callLater(function () {
|
||||
@@ -154,6 +191,27 @@ Item {
|
||||
if (saveAction === "paste") {
|
||||
ClipboardService.pasteClipboard(modal.hide);
|
||||
}
|
||||
};
|
||||
|
||||
if (entryId > 0) {
|
||||
ClipboardService.editEntry(root.entry, root.editorText, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
||||
return;
|
||||
}
|
||||
onComplete();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("clipboard.copy", {
|
||||
"text": root.editorText
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
||||
return;
|
||||
}
|
||||
onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,6 +350,7 @@ Item {
|
||||
|
||||
width: cancelButton.width
|
||||
height: buttonHeight
|
||||
opacity: root.textLoaded ? 1 : 0.6
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -181,11 +181,44 @@ FocusScope {
|
||||
}
|
||||
|
||||
function editEntry(entry) {
|
||||
if (!entry || entry.isImage) {
|
||||
if (!ClipboardService.canEditEntry(entry)) {
|
||||
return;
|
||||
}
|
||||
editorView.setEntry(entry);
|
||||
mode = "editor";
|
||||
|
||||
const requestedId = entry.id;
|
||||
if (!requestedId) {
|
||||
editorView.setEntry(entry);
|
||||
mode = "editor";
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("clipboard.getEntry", {
|
||||
"id": requestedId
|
||||
}, function (response) {
|
||||
if (response.error || !response.result) {
|
||||
ToastService.showError(I18n.tr("Failed to load clipboard entry", "clipboard editor: fetching the entry's full text failed"));
|
||||
if (!response.result) {
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const result = response.result;
|
||||
let fullText = "";
|
||||
if (result?.data) {
|
||||
fullText = editorView.decodeEntryData(result.data);
|
||||
} else {
|
||||
fullText = result?.preview ?? "";
|
||||
}
|
||||
|
||||
if (!fullText && (entry.preview ?? "").length > 0) {
|
||||
ToastService.showError(I18n.tr("Failed to load clipboard entry", "clipboard editor: fetching the entry's full text failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
const entryWithText = Object.assign({}, entry, { "text": fullText });
|
||||
editorView.setEntry(entryWithText);
|
||||
mode = "editor";
|
||||
});
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
|
||||
@@ -46,6 +46,8 @@ DankFloatingWindow {
|
||||
}
|
||||
|
||||
function submitAndClose() {
|
||||
if (!root.visible)
|
||||
return;
|
||||
if (aqueousWorkspace || CompositorService.isAqueous) {
|
||||
if (!aqueousWorkspace || renaming)
|
||||
return;
|
||||
|
||||
@@ -34,6 +34,10 @@ PluginComponent {
|
||||
TailscaleService.connectTailscale(null);
|
||||
}
|
||||
|
||||
onCcWidgetExpanded: {
|
||||
TailscaleService.refresh(null);
|
||||
}
|
||||
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
id: detailRoot
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "../../Common/WheelInput.js" as WheelInput
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
@@ -93,16 +94,15 @@ MouseArea {
|
||||
|
||||
const deltaY = wheel.angleDelta.y;
|
||||
const deltaX = wheel.angleDelta.x;
|
||||
const isTouchpadY = wheel.pixelDelta && wheel.pixelDelta.y !== 0;
|
||||
const isTouchpadX = wheel.pixelDelta && wheel.pixelDelta.x !== 0;
|
||||
const isTouchpad = WheelInput.isTouchpad(wheel);
|
||||
|
||||
if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
accumulateX(isTouchpadX, deltaX, xBehavior);
|
||||
accumulateX(isTouchpad, deltaX, xBehavior);
|
||||
return;
|
||||
}
|
||||
if (yBehavior === "none")
|
||||
return;
|
||||
accumulateY(isTouchpadY, deltaY, yBehavior);
|
||||
accumulateY(isTouchpad, deltaY, yBehavior);
|
||||
}
|
||||
|
||||
onWheel: wheel => processWheel(wheel)
|
||||
|
||||
@@ -103,8 +103,8 @@ Item {
|
||||
|
||||
function triggerControlCenterOnFocusedScreen() {
|
||||
let focusedScreenName = "";
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
|
||||
if (CompositorService.isHyprland && Hyprland.focusedMonitor) {
|
||||
focusedScreenName = Hyprland.focusedMonitor.name;
|
||||
} else if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
focusedScreenName = NiriService.currentOutput;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
@@ -132,8 +132,8 @@ Item {
|
||||
|
||||
function triggerWallpaperBrowserOnFocusedScreen() {
|
||||
let focusedScreenName = "";
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
|
||||
if (CompositorService.isHyprland && Hyprland.focusedMonitor) {
|
||||
focusedScreenName = Hyprland.focusedMonitor.name;
|
||||
} else if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
focusedScreenName = NiriService.currentOutput;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
|
||||
@@ -1168,20 +1168,8 @@ Item {
|
||||
HoverHandler {
|
||||
id: hoverPopoutHandler
|
||||
enabled: (barConfig?.hoverPopouts ?? false) && !barWindow.clickThroughEnabled
|
||||
|
||||
property real lastGlobalX: 0
|
||||
property real lastGlobalY: 0
|
||||
|
||||
onPointChanged: {
|
||||
const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y);
|
||||
lastGlobalX = gp.x;
|
||||
lastGlobalY = gp.y;
|
||||
topBarContent.queueHoverPopout(gp.x, gp.y);
|
||||
}
|
||||
|
||||
onHoveredChanged: {
|
||||
topBarContent.updateHoverBarHovered(hovered);
|
||||
}
|
||||
onPointChanged: topBarContent.queueHoverFromItem(barUnitInset, point)
|
||||
onHoveredChanged: topBarContent.updateHoverBarHovered(hovered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ Item {
|
||||
property var leftWidgetsModel
|
||||
property var centerWidgetsModel
|
||||
property var rightWidgetsModel
|
||||
property var widgetOwner: null
|
||||
property var hoverSections: null
|
||||
property bool _animateFrameInsets: false
|
||||
|
||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||
@@ -85,6 +87,34 @@ Item {
|
||||
property alias vCenterSection: vCenterSection
|
||||
property alias vRightSection: vRightSection
|
||||
|
||||
readonly property var _defaultHoverSections: barWindow.isVertical ? [
|
||||
{
|
||||
section: vLeftSection,
|
||||
name: "left"
|
||||
},
|
||||
{
|
||||
section: vCenterSection,
|
||||
name: "center"
|
||||
},
|
||||
{
|
||||
section: vRightSection,
|
||||
name: "right"
|
||||
}
|
||||
] : [
|
||||
{
|
||||
section: hLeftSection,
|
||||
name: "left"
|
||||
},
|
||||
{
|
||||
section: hCenterSection,
|
||||
name: "center"
|
||||
},
|
||||
{
|
||||
section: hRightSection,
|
||||
name: "right"
|
||||
}
|
||||
]
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: _leftMargin
|
||||
anchors.rightMargin: _rightMargin
|
||||
@@ -413,12 +443,8 @@ Item {
|
||||
barContent: topBarContent
|
||||
barWindow: topBarContent.barWindow
|
||||
barConfig: topBarContent.barConfig
|
||||
hLeftSection: topBarContent.hLeftSection
|
||||
hCenterSection: topBarContent.hCenterSection
|
||||
hRightSection: topBarContent.hRightSection
|
||||
vLeftSection: topBarContent.vLeftSection
|
||||
vCenterSection: topBarContent.vCenterSection
|
||||
vRightSection: topBarContent.vRightSection
|
||||
widgetOwner: topBarContent.widgetOwner
|
||||
sections: topBarContent.hoverSections || topBarContent._defaultHoverSections
|
||||
leftWidgetsModel: topBarContent.leftWidgetsModel
|
||||
centerWidgetsModel: topBarContent.centerWidgetsModel
|
||||
rightWidgetsModel: topBarContent.rightWidgetsModel
|
||||
@@ -427,6 +453,28 @@ Item {
|
||||
readonly property string activeHoverTrigger: hoverController.activeHoverTrigger
|
||||
readonly property bool hoverPopoutsEnabled: hoverController.hoverPopoutsEnabled
|
||||
|
||||
function mapItemToScreen(item, x, y) {
|
||||
if (!item || typeof item.mapToItem !== "function")
|
||||
return null;
|
||||
try {
|
||||
const raw = item.mapToItem(null, x ?? 0, y ?? 0);
|
||||
if (!raw)
|
||||
return null;
|
||||
return Qt.point(raw.x + (barWindow?.hostOriginX ?? 0), raw.y + (barWindow?.hostOriginY ?? 0));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function queueHoverFromItem(item, point) {
|
||||
if (!point)
|
||||
return;
|
||||
const gp = mapItemToScreen(item, point.position.x, point.position.y);
|
||||
if (!gp)
|
||||
return;
|
||||
queueHoverPopout(gp.x, gp.y);
|
||||
}
|
||||
|
||||
function queueHoverPopout(gx, gy) {
|
||||
hoverController.queueHoverPoint(gx, gy);
|
||||
}
|
||||
@@ -447,6 +495,10 @@ Item {
|
||||
hoverController.updateBarHovered(hovered);
|
||||
}
|
||||
|
||||
function invalidateHoverCandidateCache() {
|
||||
hoverController.invalidateCandidateCache();
|
||||
}
|
||||
|
||||
function resetHoverForBarGeometryChange() {
|
||||
hoverController.resetForBarGeometryChange();
|
||||
}
|
||||
@@ -465,14 +517,13 @@ Item {
|
||||
const centerSection = barWindow.isVertical ? vCenterSection : hCenterSection;
|
||||
if (centerSection) {
|
||||
if (barWindow.isVertical) {
|
||||
const centerY = centerSection.height / 2;
|
||||
return {
|
||||
triggerPos: centerSection.mapToItem(null, 0, centerY),
|
||||
triggerPos: mapItemToScreen(centerSection, 0, centerSection.height / 2),
|
||||
triggerWidth: centerSection.height
|
||||
};
|
||||
}
|
||||
return {
|
||||
triggerPos: centerSection.mapToItem(null, 0, 0),
|
||||
triggerPos: mapItemToScreen(centerSection, 0, 0),
|
||||
triggerWidth: centerSection.width
|
||||
};
|
||||
}
|
||||
@@ -480,7 +531,7 @@ Item {
|
||||
const ref = opts.visualItem || widgetItem.visualContent || widgetItem;
|
||||
const w = opts.triggerWidth !== undefined ? opts.triggerWidth : (widgetItem.visualWidth !== undefined ? widgetItem.visualWidth : widgetItem.width);
|
||||
return {
|
||||
triggerPos: ref.mapToItem(null, 0, 0),
|
||||
triggerPos: mapItemToScreen(ref, 0, 0),
|
||||
triggerWidth: w
|
||||
};
|
||||
}
|
||||
@@ -987,7 +1038,9 @@ Item {
|
||||
if (loader.item.setBarContext)
|
||||
loader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
||||
if (loader.item.setTriggerPosition) {
|
||||
const globalPos = launcherButton.visualContent.mapToItem(null, 0, 0);
|
||||
const globalPos = topBarContent.mapItemToScreen(launcherButton.visualContent, 0, 0);
|
||||
if (!globalPos)
|
||||
return false;
|
||||
const currentScreen = barWindow.screen;
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, launcherButton.visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
|
||||
loader.item.setTriggerPosition(pos.x, pos.y, pos.width, launcherButton.section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
|
||||
|
||||
@@ -10,12 +10,8 @@ Item {
|
||||
required property var barContent
|
||||
required property var barWindow
|
||||
required property var barConfig
|
||||
required property var hLeftSection
|
||||
required property var hCenterSection
|
||||
required property var hRightSection
|
||||
required property var vLeftSection
|
||||
required property var vCenterSection
|
||||
required property var vRightSection
|
||||
property var widgetOwner: null
|
||||
property var sections: []
|
||||
|
||||
property var leftWidgetsModel
|
||||
property var centerWidgetsModel
|
||||
@@ -46,6 +42,7 @@ Item {
|
||||
onLeftWidgetsModelChanged: invalidateCandidateCache()
|
||||
onCenterWidgetsModelChanged: invalidateCandidateCache()
|
||||
onRightWidgetsModelChanged: invalidateCandidateCache()
|
||||
onSectionsChanged: invalidateCandidateCache()
|
||||
|
||||
onHoverPopoutsEnabledChanged: {
|
||||
if (hoverPopoutsEnabled)
|
||||
@@ -62,7 +59,7 @@ Item {
|
||||
Component.onDestruction: _disconnectCandidateWatchers()
|
||||
|
||||
Connections {
|
||||
target: root.barContent
|
||||
target: root.widgetOwner || root.barContent
|
||||
|
||||
function onWidthChanged() {
|
||||
root.invalidateCandidateCache();
|
||||
@@ -227,41 +224,12 @@ Item {
|
||||
}
|
||||
|
||||
function _getBarSections() {
|
||||
if (barWindow.isVertical) {
|
||||
return [
|
||||
{
|
||||
section: vLeftSection,
|
||||
name: "left"
|
||||
},
|
||||
{
|
||||
section: vCenterSection,
|
||||
name: "center"
|
||||
},
|
||||
{
|
||||
section: vRightSection,
|
||||
name: "right"
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
section: hLeftSection,
|
||||
name: "left"
|
||||
},
|
||||
{
|
||||
section: hCenterSection,
|
||||
name: "center"
|
||||
},
|
||||
{
|
||||
section: hRightSection,
|
||||
name: "right"
|
||||
}
|
||||
];
|
||||
return root.sections || [];
|
||||
}
|
||||
|
||||
// The widget registry is keyed by (widgetId, screenName)
|
||||
function _itemBelongsToThisBar(item) {
|
||||
const owner = barContent;
|
||||
const owner = root.widgetOwner || barContent;
|
||||
if (!owner || !item)
|
||||
return true;
|
||||
let node = item;
|
||||
@@ -489,17 +457,15 @@ Item {
|
||||
}
|
||||
|
||||
function _globalItemBounds(item) {
|
||||
try {
|
||||
const topLeft = item.mapToItem(null, 0, 0);
|
||||
return {
|
||||
x: topLeft.x,
|
||||
y: topLeft.y,
|
||||
width: item.width,
|
||||
height: item.height
|
||||
};
|
||||
} catch (e) {
|
||||
const topLeft = barContent.mapItemToScreen(item, 0, 0);
|
||||
if (!topLeft)
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
x: topLeft.x,
|
||||
y: topLeft.y,
|
||||
width: item.width,
|
||||
height: item.height
|
||||
};
|
||||
}
|
||||
|
||||
function _hitBoundsForWidget(widgetItem, wrapper) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import qs.Widgets
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "")
|
||||
readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedMonitor ? (Hyprland.focusedMonitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "")
|
||||
readonly property string targetScreenName: parentScreen?.name || focusedScreenName
|
||||
|
||||
function resolveNotepadInstance() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Wayland
|
||||
@@ -39,13 +40,33 @@ BasePill {
|
||||
return !hiddenTrayIds.includes(itemId.toLowerCase());
|
||||
});
|
||||
}
|
||||
property var _trayKeyByItem: new Map()
|
||||
function getTrayItemKey(item) {
|
||||
const id = item?.id || "";
|
||||
const tooltipTitle = item?.tooltipTitle || "";
|
||||
if (!tooltipTitle || tooltipTitle === id) {
|
||||
return id;
|
||||
if (!id)
|
||||
return "";
|
||||
if (root._trayKeyByItem.has(item))
|
||||
return root._trayKeyByItem.get(item);
|
||||
const taken = new Set();
|
||||
for (const live of root.allTrayItems) {
|
||||
if (live !== item && (live?.id || "") === id && root._trayKeyByItem.has(live))
|
||||
taken.add(root._trayKeyByItem.get(live));
|
||||
}
|
||||
return `${id}::${tooltipTitle}`;
|
||||
let key = id;
|
||||
for (let n = 1; taken.has(key); n++)
|
||||
key = `${id}::${n}`;
|
||||
root._trayKeyByItem.set(item, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function resolveOrderIndex(key, orderMap) {
|
||||
if (orderMap.has(key))
|
||||
return orderMap.get(key);
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isTrayIdHidden(key) {
|
||||
return SessionData.isHiddenTrayId(key);
|
||||
}
|
||||
|
||||
function trayIconSourceFor(trayItem) {
|
||||
@@ -149,6 +170,13 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SystemTray
|
||||
function onItemUnregistered(item) {
|
||||
root._trayKeyByItem.delete(item);
|
||||
}
|
||||
}
|
||||
|
||||
function sortByPreferredOrder(items, trigger) {
|
||||
void trigger;
|
||||
const savedOrder = SessionData.trayItemOrder || [];
|
||||
@@ -158,15 +186,17 @@ BasePill {
|
||||
return [...items].sort((a, b) => {
|
||||
const keyA = getTrayItemKey(a);
|
||||
const keyB = getTrayItemKey(b);
|
||||
const orderA = orderMap.has(keyA) ? orderMap.get(keyA) : 10000 + items.indexOf(a);
|
||||
const orderB = orderMap.has(keyB) ? orderMap.get(keyB) : 10000 + items.indexOf(b);
|
||||
return orderA - orderB;
|
||||
const orderA = resolveOrderIndex(keyA, orderMap);
|
||||
const orderB = resolveOrderIndex(keyB, orderMap);
|
||||
const posA = orderA >= 0 ? orderA : 10000 + items.indexOf(a);
|
||||
const posB = orderB >= 0 ? orderB : 10000 + items.indexOf(b);
|
||||
return posA - posB;
|
||||
});
|
||||
}
|
||||
|
||||
readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger)
|
||||
readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item))
|
||||
readonly property var visibleSortedTrayItems: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
|
||||
readonly property var visibleSortedTrayItems: allSortedTrayItems.filter(item => !root.isTrayIdHidden(root.getTrayItemKey(item)))
|
||||
readonly property int automaticVisibleItemLimit: {
|
||||
if (!root.useAutomaticOverflow)
|
||||
return root.visibleSortedTrayItems.length;
|
||||
@@ -189,7 +219,7 @@ BasePill {
|
||||
item: item
|
||||
}))
|
||||
readonly property var autoOverflowBarItems: visibleSortedTrayItems.slice(automaticVisibleItemLimit)
|
||||
readonly property var manualHiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
|
||||
readonly property var manualHiddenBarItems: allSortedTrayItems.filter(item => root.isTrayIdHidden(root.getTrayItemKey(item)))
|
||||
readonly property var hiddenBarItemKeys: manualHiddenBarItems.concat(autoOverflowBarItems).map(item => root.getTrayItemKey(item))
|
||||
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => hiddenBarItemKeys.indexOf(root.getTrayItemKey(item)) !== -1)
|
||||
readonly property string trayIconTintMode: {
|
||||
@@ -297,7 +327,7 @@ BasePill {
|
||||
}
|
||||
|
||||
function isManualHiddenTrayItem(item) {
|
||||
return SessionData.isHiddenTrayId(getTrayItemKey(item));
|
||||
return root.isTrayIdHidden(root.getTrayItemKey(item));
|
||||
}
|
||||
|
||||
function isAutoOverflowTrayItem(item) {
|
||||
@@ -2028,7 +2058,7 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
@@ -2038,9 +2068,9 @@ BasePill {
|
||||
visible: !menuEntry?.isSeparator
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSizeSmall
|
||||
height: Theme.iconSizeSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: Theme.iconSizeSmall
|
||||
Layout.preferredHeight: Theme.iconSizeSmall
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
|
||||
radius: menuEntry?.buttonType === 2 ? width / 2 : 2
|
||||
border.width: 1
|
||||
@@ -2066,9 +2096,9 @@ BasePill {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Theme.iconSizeSmall
|
||||
height: Theme.iconSizeSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: Theme.iconSizeSmall
|
||||
Layout.preferredHeight: Theme.iconSizeSmall
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: (menuEntry?.icon ?? "") !== ""
|
||||
|
||||
Image {
|
||||
@@ -2086,22 +2116,23 @@ BasePill {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.max(150, parent.width - 64)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 0
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Theme.iconSizeSmall
|
||||
height: Theme.iconSizeSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: Theme.iconSizeSmall
|
||||
Layout.preferredHeight: Theme.iconSizeSmall
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: menuEntry?.hasChildren ?? false
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "chevron_right"
|
||||
size: Theme.iconSizeSmall - 2
|
||||
color: Theme.widgetTextColor
|
||||
visible: menuEntry?.hasChildren ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Quickshell.WindowManager
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/WheelInput.js" as WheelInput
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -992,7 +993,7 @@ Item {
|
||||
return;
|
||||
|
||||
const delta = wheel.angleDelta.y;
|
||||
const isTouchpad = wheel.pixelDelta && wheel.pixelDelta.y !== 0;
|
||||
const isTouchpad = WheelInput.isTouchpad(wheel);
|
||||
const reverse = SettingsData.reverseScrolling ? -1 : 1;
|
||||
|
||||
if (isTouchpad) {
|
||||
|
||||
@@ -194,7 +194,6 @@ Item {
|
||||
id: mediaExpandedLoader
|
||||
|
||||
activity: "media"
|
||||
height: Math.max(target.height, root.height)
|
||||
active: root.mediaSurfaceActive && (root.expanded || root.expandedFade > 0)
|
||||
asynchronous: false
|
||||
sourceComponent: root.mediaExpandedComponent
|
||||
|
||||
@@ -153,6 +153,9 @@ Item {
|
||||
property bool hasAdjacentLeftBar: false
|
||||
property bool hasAdjacentRightBar: false
|
||||
property var hyprlandOverviewLoader: root.hyprlandOverviewLoader
|
||||
property var hostWindow: root.hostWindow
|
||||
readonly property int hostOriginX: hostWindow?.hostOriginX ?? 0
|
||||
readonly property int hostOriginY: hostWindow?.hostOriginY ?? 0
|
||||
property var controlCenterButtonRef: null
|
||||
property var clockButtonRef: null
|
||||
property var systemUpdateButtonRef: null
|
||||
@@ -269,9 +272,16 @@ Item {
|
||||
return;
|
||||
blurRebuildTimer.restart();
|
||||
blurTrailTimer.restart();
|
||||
componentProvider.invalidateHoverCandidateCache();
|
||||
}
|
||||
}
|
||||
|
||||
function onSatelliteRowMoved() {
|
||||
kickBlur();
|
||||
if (!root.motionRunning)
|
||||
componentProvider.invalidateHoverCandidateCache();
|
||||
}
|
||||
|
||||
function kickBlur() {
|
||||
if (root.motionRunning)
|
||||
return;
|
||||
@@ -412,11 +422,26 @@ Item {
|
||||
barWindow: satelliteBarWindow
|
||||
rootWindow: root.hostWindow
|
||||
barConfig: root.satelliteConfig
|
||||
widgetOwner: root
|
||||
hoverSections: [
|
||||
{
|
||||
section: leadingWidgetSection,
|
||||
name: "left"
|
||||
},
|
||||
{
|
||||
section: trailingWidgetSection,
|
||||
name: "right"
|
||||
}
|
||||
]
|
||||
leftWidgetsModel: emptyWidgetsModel
|
||||
centerWidgetsModel: emptyWidgetsModel
|
||||
rightWidgetsModel: emptyWidgetsModel
|
||||
}
|
||||
|
||||
readonly property bool satelliteHoverTracking: root.visible && componentProvider.hoverPopoutsEnabled && !root.controller.inputSuspended
|
||||
readonly property bool satellitesHovered: satelliteHoverTracking && (leadingEnvelopeHoverHandler.hovered || trailingEnvelopeHoverHandler.hovered || leadingWidgetHoverHandler.hovered || trailingWidgetHoverHandler.hovered)
|
||||
onSatellitesHoveredChanged: componentProvider.updateHoverBarHovered(satellitesHovered)
|
||||
|
||||
Item {
|
||||
id: leadingInputEnvelope
|
||||
|
||||
@@ -429,6 +454,12 @@ Item {
|
||||
y: root.isVertical ? alongPos : crossPos
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
|
||||
HoverHandler {
|
||||
id: leadingEnvelopeHoverHandler
|
||||
enabled: root.satelliteHoverTracking
|
||||
onPointChanged: componentProvider.queueHoverFromItem(leadingInputEnvelope, point)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -444,6 +475,12 @@ Item {
|
||||
y: root.isVertical ? alongPos : crossPos
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
|
||||
HoverHandler {
|
||||
id: trailingEnvelopeHoverHandler
|
||||
enabled: root.satelliteHoverTracking
|
||||
onPointChanged: componentProvider.queueHoverFromItem(trailingInputEnvelope, point)
|
||||
}
|
||||
}
|
||||
|
||||
IslandSatelliteChrome {
|
||||
@@ -500,8 +537,10 @@ Item {
|
||||
y: root.isVertical ? alongPos : root.rowCross
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
onAlongPosChanged: root.kickBlur()
|
||||
onAlongSizeChanged: root.kickBlur()
|
||||
onXChanged: root.onSatelliteRowMoved()
|
||||
onYChanged: root.onSatelliteRowMoved()
|
||||
onWidthChanged: root.onSatelliteRowMoved()
|
||||
onHeightChanged: root.onSatelliteRowMoved()
|
||||
|
||||
LeftSection {
|
||||
id: leadingWidgetSection
|
||||
@@ -521,6 +560,12 @@ Item {
|
||||
crossEdgeExtension: root.crossEdgeExtension
|
||||
edgeIsScreenEdge: root.edgeAligned
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: leadingWidgetHoverHandler
|
||||
enabled: root.satelliteHoverTracking
|
||||
onPointChanged: componentProvider.queueHoverFromItem(leadingInput, point)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -534,8 +579,10 @@ Item {
|
||||
y: root.isVertical ? alongPos : root.rowCross
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
onAlongPosChanged: root.kickBlur()
|
||||
onAlongSizeChanged: root.kickBlur()
|
||||
onXChanged: root.onSatelliteRowMoved()
|
||||
onYChanged: root.onSatelliteRowMoved()
|
||||
onWidthChanged: root.onSatelliteRowMoved()
|
||||
onHeightChanged: root.onSatelliteRowMoved()
|
||||
|
||||
RightSection {
|
||||
id: trailingWidgetSection
|
||||
@@ -555,5 +602,11 @@ Item {
|
||||
crossEdgeExtension: root.crossEdgeExtension
|
||||
edgeIsScreenEdge: root.edgeAligned
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: trailingWidgetHoverHandler
|
||||
enabled: root.satelliteHoverTracking
|
||||
onPointChanged: componentProvider.queueHoverFromItem(trailingInput, point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ Item {
|
||||
onTriggered: dock.startupRevealDone = true
|
||||
}
|
||||
|
||||
readonly property bool overviewReveal: CompositorService.overviewActiveOnScreen(screenName) && SettingsData.dockOpenOnOverview
|
||||
readonly property bool overviewReveal: CompositorService.overviewActiveOnScreen(dock._dockScreenName) && SettingsData.dockOpenOnOverview
|
||||
readonly property bool hoverOrActive: dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
|
||||
|
||||
onOverviewRevealChanged: {
|
||||
|
||||
@@ -43,7 +43,7 @@ Variants {
|
||||
if (launcherOpen || _openedForCurrentHover)
|
||||
return;
|
||||
_openedForCurrentHover = true;
|
||||
PopoutService.openDankLauncherV2(CompositorService.framePeerSurfacesUseOverlayForScreen(zoneLoader.modelData), true);
|
||||
PopoutService.openDankLauncherV2(false, true);
|
||||
}
|
||||
|
||||
screen: zoneLoader.modelData
|
||||
|
||||
@@ -272,7 +272,8 @@ Scope {
|
||||
property bool isActiveScreen: {
|
||||
if (Quickshell.screens.length <= 1)
|
||||
return true;
|
||||
return SettingsData.getFilteredScreens("lockScreen").includes(screen);
|
||||
const screens = SettingsData.getFilteredScreens("lockScreen");
|
||||
return screens.length === 0 || screens.includes(screen);
|
||||
}
|
||||
|
||||
color: isActiveScreen ? "transparent" : SettingsData.lockScreenInactiveColor
|
||||
|
||||
@@ -82,6 +82,8 @@ Item {
|
||||
return I18n.tr("Too many attempts - locked out");
|
||||
if (root.pamState === "fail")
|
||||
return I18n.tr("Incorrect password - try again");
|
||||
if (pam.fprint.status === "disabled")
|
||||
return "";
|
||||
if (pam.fprintState === "error")
|
||||
return I18n.tr("Fingerprint error");
|
||||
if (pam.fprintState === "max")
|
||||
@@ -861,10 +863,15 @@ Item {
|
||||
name: {
|
||||
if (pam.u2fPending)
|
||||
return "passkey";
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
switch (pam.fprint.status) {
|
||||
case "max":
|
||||
case "stopped":
|
||||
return "fingerprint_off";
|
||||
if (pam.fprint.active || pam.fprint.retrying)
|
||||
case "active":
|
||||
return "fingerprint";
|
||||
case "retrying":
|
||||
return "hourglass_empty";
|
||||
}
|
||||
if (pam.u2f.active)
|
||||
return "passkey";
|
||||
return "lock";
|
||||
@@ -1005,6 +1012,8 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
root.pam.retryFprintOnActivity();
|
||||
|
||||
if (root.unlocking) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
|
||||
@@ -16,7 +16,20 @@ FocusScope {
|
||||
signal passwordChanged(string newPassword)
|
||||
signal unlockRequested
|
||||
|
||||
HoverHandler {
|
||||
enabled: root.pam.awaitingActivityRetry
|
||||
onPointChanged: root.pam.retryFprintOnActivity()
|
||||
}
|
||||
|
||||
PointHandler {
|
||||
onActiveChanged: {
|
||||
if (active)
|
||||
root.pam.retryFprintOnActivity();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
root.pam.retryFprintOnActivity();
|
||||
if (videoScreensaver.active && videoScreensaver.inputEnabled) {
|
||||
videoScreensaver.dismiss();
|
||||
event.accepted = true;
|
||||
|
||||
+154
-52
@@ -32,9 +32,8 @@ Scope {
|
||||
|
||||
function resetAuthFlows(): void {
|
||||
passwd.abort();
|
||||
fprint.abort();
|
||||
fprint.stop();
|
||||
u2f.abort();
|
||||
errorRetry.running = false;
|
||||
u2fErrorRetry.running = false;
|
||||
u2fPendingTimeout.running = false;
|
||||
passwdActiveTimeout.running = false;
|
||||
@@ -58,9 +57,8 @@ Scope {
|
||||
if (!root.unlockInProgress) {
|
||||
root.unlockInProgress = true;
|
||||
passwd.abort();
|
||||
fprint.abort();
|
||||
fprint.stop();
|
||||
u2f.abort();
|
||||
errorRetry.running = false;
|
||||
u2fErrorRetry.running = false;
|
||||
u2fPendingTimeout.running = false;
|
||||
root.u2fPending = false;
|
||||
@@ -217,7 +215,7 @@ Scope {
|
||||
|
||||
if (res === PamResult.Success) {
|
||||
if (!root.unlockInProgress) {
|
||||
fprint.abort();
|
||||
fprint.stop();
|
||||
root.proceedAfterPrimaryAuth();
|
||||
}
|
||||
return;
|
||||
@@ -260,69 +258,147 @@ Scope {
|
||||
PamContext {
|
||||
id: fprint
|
||||
|
||||
property bool available: SettingsData.lockFingerprintReady
|
||||
property int tries
|
||||
property int errorTries
|
||||
property bool retrying: false
|
||||
readonly property bool available: SettingsData.lockFingerprintReady
|
||||
property int tries: 0
|
||||
property int errorTries: 0
|
||||
property double attemptStartedAt: 0
|
||||
property bool completedDuringStart: false
|
||||
readonly property int maxErrorTries: 200
|
||||
readonly property int daemonIdleExitMs: 30000
|
||||
// sessionTimeoutMs mirrors `timeout=` in assets/pam/fprint
|
||||
readonly property int sessionTimeoutMs: 90000
|
||||
readonly property int verifyStartSlackMs: 10000
|
||||
readonly property bool retrying: errorRetry.running
|
||||
readonly property int retryInterval: errorRetry.interval
|
||||
readonly property bool allowed: available && SettingsData.enableFprint && root.lockSecured && !root.fprintSuppressedByPrimaryPam && !root.unlockInProgress && !root.u2fPending && !SessionService.preparingForSleep && !IdleService.monitorsOff && tries < SettingsData.maxFprintTries
|
||||
readonly property string status: {
|
||||
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam)
|
||||
return "disabled";
|
||||
if (tries >= SettingsData.maxFprintTries)
|
||||
return "max";
|
||||
if (!allowed)
|
||||
return "paused";
|
||||
if (active)
|
||||
return "active";
|
||||
if (retrying)
|
||||
return "retrying";
|
||||
return errorTries >= maxErrorTries ? "stopped" : "idle";
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
errorRetry.stop();
|
||||
attemptStartedAt = 0;
|
||||
abort();
|
||||
}
|
||||
|
||||
// pam_fprintd reports an expired timeout as PAM_AUTHINFO_UNAVAIL, the same
|
||||
// result a device fault gives, and its "Verification timed out" message is
|
||||
// translated, so age is the signal we can rely on. Its timeout runs from the
|
||||
// verify rather than from the claim, which is why the age is measured from
|
||||
// the message announcing the verify: a slow claim then cannot make a fault
|
||||
// look old enough, and a session that never reached a verify never qualifies.
|
||||
// That message lands after pam_fprintd has already armed its deadline, so the
|
||||
// age read here runs short of the real timeout and the slack makes it up. The
|
||||
// bias is deliberate: a timeout misread as a fault parks the reader behind the
|
||||
// backoff, while a fault misread as a timeout costs one extra retry.
|
||||
function attemptSettled(): bool {
|
||||
return attemptStartedAt > 0 && Date.now() - attemptStartedAt >= sessionTimeoutMs - verifyStartSlackMs;
|
||||
}
|
||||
|
||||
function checkAvail(): void {
|
||||
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
|
||||
retrying = false;
|
||||
abort();
|
||||
if (!allowed) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (!active && !retrying)
|
||||
startAttempt();
|
||||
}
|
||||
|
||||
function startAttempt(): void {
|
||||
if (!allowed || errorTries >= maxErrorTries) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (active)
|
||||
return;
|
||||
errorRetry.stop();
|
||||
if (root.fprintState === "error")
|
||||
root.fprintState = "";
|
||||
attemptStartedAt = 0;
|
||||
completedDuringStart = false;
|
||||
if (start())
|
||||
return;
|
||||
if (!completedDuringStart)
|
||||
scheduleErrorRetry();
|
||||
}
|
||||
|
||||
tries = 0;
|
||||
function scheduleErrorRetry(): void {
|
||||
stop();
|
||||
if (!allowed)
|
||||
return;
|
||||
errorTries++;
|
||||
root.fprintState = "error";
|
||||
if (errorTries === 1)
|
||||
root.flashMsg();
|
||||
fprintStateReset.restart();
|
||||
if (errorTries < maxErrorTries)
|
||||
errorRetry.restart();
|
||||
}
|
||||
|
||||
function recover(): void {
|
||||
if (!allowed)
|
||||
return;
|
||||
errorTries = 0;
|
||||
retrying = false;
|
||||
start();
|
||||
if (!active)
|
||||
startAttempt();
|
||||
}
|
||||
|
||||
onAllowedChanged: {
|
||||
if (!allowed)
|
||||
stop();
|
||||
else
|
||||
Qt.callLater(checkAvail);
|
||||
}
|
||||
|
||||
config: "fprint"
|
||||
configDirectory: Quickshell.shellDir + "/assets/pam"
|
||||
|
||||
onPamMessage: {
|
||||
if (attemptStartedAt === 0)
|
||||
attemptStartedAt = Date.now();
|
||||
}
|
||||
|
||||
onCompleted: res => {
|
||||
if (!available)
|
||||
completedDuringStart = true;
|
||||
if (!allowed)
|
||||
return;
|
||||
|
||||
switch (res) {
|
||||
case PamResult.Success:
|
||||
retrying = false;
|
||||
if (!root.unlockInProgress) {
|
||||
passwd.abort();
|
||||
root.proceedAfterPrimaryAuth();
|
||||
}
|
||||
stop();
|
||||
passwd.abort();
|
||||
root.proceedAfterPrimaryAuth();
|
||||
return;
|
||||
case PamResult.Error:
|
||||
errorTries++;
|
||||
if (errorTries < 200) {
|
||||
retrying = true;
|
||||
abort();
|
||||
errorRetry.restart();
|
||||
if (attemptSettled()) {
|
||||
stop();
|
||||
errorTries = 0;
|
||||
startAttempt();
|
||||
return;
|
||||
}
|
||||
retrying = false;
|
||||
abort();
|
||||
scheduleErrorRetry();
|
||||
return;
|
||||
case PamResult.Failed:
|
||||
case PamResult.MaxTries:
|
||||
retrying = false;
|
||||
stop();
|
||||
tries++;
|
||||
if (tries < SettingsData.maxFprintTries) {
|
||||
root.fprintState = "fail";
|
||||
start();
|
||||
} else {
|
||||
root.fprintState = "max";
|
||||
abort();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
errorTries = 0;
|
||||
root.fprintState = tries < SettingsData.maxFprintTries ? "fail" : "max";
|
||||
root.flashMsg();
|
||||
fprintStateReset.restart();
|
||||
startAttempt();
|
||||
return;
|
||||
}
|
||||
|
||||
root.flashMsg();
|
||||
fprintStateReset.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,20 +502,35 @@ Scope {
|
||||
Timer {
|
||||
id: errorRetry
|
||||
|
||||
interval: Math.min(1500 * Math.pow(2, Math.max(0, fprint.errorTries - 1)), 30000)
|
||||
onTriggered: fprint.start()
|
||||
// A device wedged by a suspend mid-verify only comes back when fprintd
|
||||
// restarts, and fprintd only exits after its idle timeout with no client
|
||||
// attached, so the second retry outlasts that instead of climbing to it.
|
||||
interval: {
|
||||
if (fprint.errorTries <= 1)
|
||||
return 1500;
|
||||
if (fprint.errorTries === 2)
|
||||
return fprint.daemonIdleExitMs + 5000;
|
||||
return 60000;
|
||||
}
|
||||
onTriggered: fprint.startAttempt()
|
||||
}
|
||||
|
||||
readonly property bool awaitingActivityRetry: fprint.errorTries > 0 && !fprint.retrying && !fprint.active
|
||||
|
||||
function retryFprintOnActivity(): void {
|
||||
if (root.awaitingActivityRetry)
|
||||
fprint.recover();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionService
|
||||
|
||||
// timers run on monotonic time, so suspend leaves the backoff remainder to burn after wake (#3171)
|
||||
function onSessionResumed() {
|
||||
if (!fprint.available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam)
|
||||
return;
|
||||
fprint.errorTries = 0;
|
||||
if (errorRetry.running)
|
||||
errorRetry.restart();
|
||||
fprint.recover();
|
||||
}
|
||||
|
||||
function onLidOpened() {
|
||||
fprint.recover();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +582,10 @@ Scope {
|
||||
id: fprintStateReset
|
||||
|
||||
interval: 4000
|
||||
onTriggered: root.fprintState = ""
|
||||
onTriggered: {
|
||||
if (root.fprintState !== "max")
|
||||
root.fprintState = "";
|
||||
}
|
||||
}
|
||||
|
||||
onLockSecuredChanged: {
|
||||
@@ -508,6 +602,8 @@ Scope {
|
||||
root.attemptInfoMessages = [];
|
||||
root.lockoutAnnouncedThisAttempt = false;
|
||||
root.resetAuthFlows();
|
||||
fprint.tries = 0;
|
||||
fprint.errorTries = 0;
|
||||
if (!SettingsData.lockPamExternallyManaged && !dankshellConfigWatcher.loaded && !userPamWatcher.loaded)
|
||||
ensureUserPamConfig();
|
||||
// FileView cannot watch a path that does not exist yet; re-read so a
|
||||
@@ -521,11 +617,17 @@ Scope {
|
||||
target: SettingsData
|
||||
|
||||
function onEnableFprintChanged(): void {
|
||||
fprint.checkAvail();
|
||||
if (SettingsData.enableFprint)
|
||||
fprint.recover();
|
||||
else
|
||||
fprint.checkAvail();
|
||||
}
|
||||
|
||||
function onLockFingerprintReadyChanged(): void {
|
||||
fprint.checkAvail();
|
||||
if (SettingsData.lockFingerprintReady)
|
||||
fprint.recover();
|
||||
else
|
||||
fprint.checkAvail();
|
||||
}
|
||||
|
||||
function onEnableU2fChanged(): void {
|
||||
|
||||
@@ -698,7 +698,7 @@ Column {
|
||||
color: Theme.surfaceTextSecondary
|
||||
font.family: textArea.font.family
|
||||
font.pixelSize: textArea.font.pixelSize
|
||||
visible: textArea.text.length === 0
|
||||
visible: textArea.text.length === 0 && !textArea.inputMethodComposing
|
||||
anchors.left: textArea.left
|
||||
anchors.top: textArea.top
|
||||
anchors.leftMargin: textArea.leftPadding
|
||||
|
||||
@@ -16,22 +16,43 @@ Item {
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
|
||||
// The sysfs names DMS can write, in the order they are tried. Hardware that
|
||||
// exposes none of them, a Lenovo IdeaPad on ideapad_laptop for instance,
|
||||
// used to run the apply script to completion without writing anything, and
|
||||
// sh exiting 0 was indistinguishable from a successful write.
|
||||
readonly property string thresholdFileList: "charge_control_limit_max charge_stop_threshold charge_control_end_threshold"
|
||||
readonly property int noThresholdExitCode: 2
|
||||
|
||||
property bool chargeLimitSupported: true
|
||||
|
||||
Process {
|
||||
id: thresholdProbe
|
||||
running: Qt.platform.os === "linux"
|
||||
command: ["sh", "-c", "for bat in /sys/class/power_supply/BAT*; do for file in " + root.thresholdFileList + "; do [ -f \"$bat/$file\" ] && exit 0; done; done; exit 1"]
|
||||
onExited: exitCode => root.chargeLimitSupported = exitCode === 0
|
||||
}
|
||||
|
||||
Process {
|
||||
id: applyLimitProcess
|
||||
command: ["pkexec", "sh", "-c", "
|
||||
found=0
|
||||
for bat in /sys/class/power_supply/BAT*; do
|
||||
if [ -f \"$bat/charge_control_limit_max\" ]; then
|
||||
echo " + SettingsData.batteryChargeLimit + " > \"$bat/charge_control_limit_max\"
|
||||
elif [ -f \"$bat/charge_stop_threshold\" ]; then
|
||||
echo " + SettingsData.batteryChargeLimit + " > \"$bat/charge_stop_threshold\"
|
||||
elif [ -f \"$bat/charge_control_end_threshold\" ]; then
|
||||
echo " + SettingsData.batteryChargeLimit + " > \"$bat/charge_control_end_threshold\"
|
||||
fi
|
||||
for file in " + root.thresholdFileList + "; do
|
||||
if [ -f \"$bat/$file\" ]; then
|
||||
found=1
|
||||
echo " + SettingsData.batteryChargeLimit + " > \"$bat/$file\" || exit 1
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
[ \"$found\" = 1 ] || exit " + root.noThresholdExitCode + "
|
||||
"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
if (exitCode === root.noThresholdExitCode) {
|
||||
root.chargeLimitSupported = false;
|
||||
ToastService.showError(I18n.tr("Charge limit not supported on this hardware", "battery settings: toast title when sysfs has no charge threshold"), I18n.tr("No writable charge threshold file was found under /sys/class/power_supply.", "battery settings: why the charge limit could not be applied"));
|
||||
} else if (exitCode !== 0) {
|
||||
ToastService.showError(I18n.tr("Failed to apply charge limit to system"), I18n.tr("Process exited with code %1").arg(exitCode));
|
||||
} else {
|
||||
ToastService.showInfo(I18n.tr("Charge limit applied successfully"), I18n.tr("Limit set to %1%").arg(SettingsData.batteryChargeLimit));
|
||||
@@ -214,9 +235,24 @@ done
|
||||
onSliderValueChanged: newValue => SettingsData.set("batteryChargeLimit", newValue)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
// A machine without a battery has no charge threshold to
|
||||
// write to either, but saying so there would be noise: the
|
||||
// limit was never applicable in the first place.
|
||||
visible: Qt.platform.os === "linux" && BatteryService.batteryAvailable && !root.chargeLimitSupported
|
||||
text: I18n.tr("No writable charge threshold file was found under /sys/class/power_supply.", "battery settings: why the charge limit could not be applied")
|
||||
wrapMode: Text.WordWrap
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
|
||||
Row {
|
||||
// charge_control_* live in Linux sysfs; no BSD equivalent
|
||||
visible: Qt.platform.os === "linux"
|
||||
// charge_control_* live in Linux sysfs; no BSD equivalent.
|
||||
// Without a battery the apply button cannot do anything,
|
||||
// and offering it anyway is what produced the false
|
||||
// success this change is about.
|
||||
visible: Qt.platform.os === "linux" && BatteryService.batteryAvailable && root.chargeLimitSupported
|
||||
width: parent.width
|
||||
height: applyButton.height
|
||||
layoutDirection: I18n.isRtl ? Qt.LeftToRight : Qt.RightToLeft
|
||||
|
||||
@@ -1113,6 +1113,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: HyprlandService
|
||||
function onMonitorLayoutChanged() {
|
||||
root.outputs = root.buildOutputsMap();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onCompositorChanged() {
|
||||
@@ -1806,6 +1813,13 @@ Singleton {
|
||||
"transform": mapWlrTransform(output.transform)
|
||||
}
|
||||
};
|
||||
const live = HyprlandService.liveMonitor(output.name);
|
||||
if (!live)
|
||||
continue;
|
||||
map[output.name].logical.x = live.x;
|
||||
map[output.name].logical.y = live.y;
|
||||
map[output.name].logical.scale = live.scale || 1.0;
|
||||
map[output.name].logical.transform = hyprlandToTransform(live.lastIpcObject?.transform ?? 0);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -244,14 +244,13 @@ StyledRect {
|
||||
width: parent.width
|
||||
dropdownWidth: parent.width
|
||||
visible: !scaleContainer.customMode
|
||||
currentValue: scaleContainer.currentScale
|
||||
options: scaleContainer.scaleOptionsData.labels
|
||||
onValueChanged: value => {
|
||||
if (value === I18n.tr("Custom...")) {
|
||||
scaleContainer.customMode = true;
|
||||
currentValue = scaleContainer.currentScale;
|
||||
scaleInput.text = scaleContainer.currentScale;
|
||||
scaleInput.forceActiveFocus();
|
||||
scaleInput.selectAll();
|
||||
scaleContainer.customMode = true;
|
||||
customFocusTimer.restart();
|
||||
return;
|
||||
}
|
||||
const mapped = scaleContainer.scaleOptionsData.valueByLabel[value];
|
||||
@@ -259,6 +258,22 @@ StyledRect {
|
||||
}
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: scaleDropdown
|
||||
property: "currentValue"
|
||||
value: scaleContainer.currentScale
|
||||
}
|
||||
|
||||
// the dropdown takes focus back when its menu finishes closing
|
||||
Timer {
|
||||
id: customFocusTimer
|
||||
interval: Theme.shortDuration + 100
|
||||
onTriggered: {
|
||||
scaleInput.forceActiveFocus();
|
||||
scaleInput.selectAll();
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: scaleInput
|
||||
width: parent.width
|
||||
|
||||
@@ -162,6 +162,7 @@ StyledRect {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankSpinner {
|
||||
size: Theme.iconSize
|
||||
running: root.isUpdating
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -881,6 +881,7 @@ EOFCONFIG
|
||||
[ $found -eq 1 ] && break
|
||||
done
|
||||
done
|
||||
exit 0
|
||||
`;
|
||||
|
||||
Proc.runCommand("discoverSoundFiles", ["sh", "-c", script], (output, exitCode) => {
|
||||
|
||||
@@ -140,8 +140,8 @@ Singleton {
|
||||
function getFocusedScreenName() {
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return AqueousService.focusedOutput;
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace?.monitor)
|
||||
return Hyprland.focusedWorkspace.monitor.name;
|
||||
if (CompositorService.isHyprland && Hyprland.focusedMonitor)
|
||||
return Hyprland.focusedMonitor.name;
|
||||
if (CompositorService.isNiri && NiriService.currentOutput)
|
||||
return NiriService.currentOutput;
|
||||
if (CompositorService.isMango && MangoService.activeOutput)
|
||||
|
||||
@@ -200,6 +200,7 @@ Singleton {
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
unpinnedEntries = [];
|
||||
pinnedEntries = [];
|
||||
}
|
||||
|
||||
function copyEntry(entry, closeCallback, textOnly) {
|
||||
@@ -334,6 +335,33 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
function editEntry(entry, text, callback) {
|
||||
if (!entry || typeof entry.id !== "number") {
|
||||
if (callback) {
|
||||
callback({
|
||||
"error": "Invalid entry"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
DMSService.sendRequest("clipboard.editEntry", {
|
||||
"id": entry.id,
|
||||
"text": text
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
log.warn("Failed to edit entry:", response.error);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
refresh();
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const hasPinned = pinnedCount > 0;
|
||||
const savedCount = pinnedCount;
|
||||
@@ -371,6 +399,17 @@ Singleton {
|
||||
return entry.preview || "";
|
||||
}
|
||||
|
||||
function isTextMimeType(mimeType) {
|
||||
if (!mimeType || mimeType.startsWith("text/plain")) {
|
||||
return true;
|
||||
}
|
||||
return mimeType === "UTF8_STRING" || mimeType === "STRING" || mimeType === "TEXT";
|
||||
}
|
||||
|
||||
function canEditEntry(entry) {
|
||||
return !!entry && !(entry.isImage ?? false) && isTextMimeType(entry.mimeType);
|
||||
}
|
||||
|
||||
function getEntryType(entry) {
|
||||
if (entry.isImage) {
|
||||
return "image";
|
||||
|
||||
@@ -202,8 +202,8 @@ Singleton {
|
||||
let screenName = "";
|
||||
if (isAqueous && AqueousService.available)
|
||||
screenName = AqueousService.focusedOutput;
|
||||
else if (isHyprland && Hyprland.focusedWorkspace?.monitor)
|
||||
screenName = Hyprland.focusedWorkspace.monitor.name;
|
||||
else if (isHyprland && Hyprland.focusedMonitor)
|
||||
screenName = Hyprland.focusedMonitor.name;
|
||||
else if (isNiri && NiriService.currentOutput)
|
||||
screenName = NiriService.currentOutput;
|
||||
else if (isSway || isScroll || isMiracle) {
|
||||
@@ -275,7 +275,7 @@ Singleton {
|
||||
enabled: isHyprland
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "monitoraddedv2" || event.name === "monitorremoved") {
|
||||
if (event.name === "monitoraddedv2" || event.name === "monitorremoved" || event.name === "configreloaded") {
|
||||
root.refreshHyprlandMonitorLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,26 @@ Singleton {
|
||||
onTriggered: root.requestSync("startup")
|
||||
}
|
||||
|
||||
property int powerSyncRecoveryAttempt: 0
|
||||
readonly property var powerSyncRecoveryIntervals: [1000, 5000, 15000, 30000]
|
||||
|
||||
Timer {
|
||||
id: powerSyncRecoveryTimer
|
||||
interval: root.powerSyncRecoveryIntervals[0]
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.requestSync("resume-reconcile");
|
||||
root.powerSyncRecoveryAttempt++;
|
||||
if (root.powerSyncRecoveryAttempt < root.powerSyncRecoveryIntervals.length) {
|
||||
interval = root.powerSyncRecoveryIntervals[root.powerSyncRecoveryAttempt];
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
root.powerSyncRecoveryAttempt = 0;
|
||||
interval = root.powerSyncRecoveryIntervals[0];
|
||||
}
|
||||
}
|
||||
|
||||
signal brightnessChanged(bool showOsd)
|
||||
signal deviceSwitched
|
||||
|
||||
@@ -1541,6 +1561,9 @@ Singleton {
|
||||
resumeRecoveryAttempt = 0;
|
||||
resumeRecoveryTimer.interval = 400;
|
||||
resumeRecoveryTimer.restart();
|
||||
powerSyncRecoveryAttempt = 0;
|
||||
powerSyncRecoveryTimer.interval = powerSyncRecoveryIntervals[0];
|
||||
powerSyncRecoveryTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ Singleton {
|
||||
readonly property bool luaConfigActive: CompositorService.isHyprland && (Hyprland.usingLua === true || luaConfigDetected)
|
||||
|
||||
property int _lastGapValue: -1
|
||||
property string _monitorLayoutSignature: ""
|
||||
signal monitorLayoutChanged
|
||||
property bool luaConfigDetected: false
|
||||
property bool luaConfigStatusReady: false
|
||||
property bool luaConfigStatusLoading: false
|
||||
@@ -296,11 +298,38 @@ Singleton {
|
||||
Proc.runCommand("hyprctl-reload", ["hyprctl", "reload"], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
log.warn("hyprctl reload failed:", output);
|
||||
else
|
||||
Hyprland.refreshMonitors();
|
||||
if (callback)
|
||||
callback(exitCode === 0);
|
||||
});
|
||||
}
|
||||
|
||||
function liveMonitor(name) {
|
||||
if (!CompositorService.isHyprland)
|
||||
return null;
|
||||
return Hyprland.monitors.values.find(m => m.name === name) ?? null;
|
||||
}
|
||||
|
||||
function _syncMonitorLayout() {
|
||||
const signature = Hyprland.monitors.values.map(m => `${m.name}:${m.x},${m.y},${m.scale},${m.lastIpcObject?.transform ?? 0}`).join("|");
|
||||
if (signature === _monitorLayoutSignature)
|
||||
return;
|
||||
_monitorLayoutSignature = signature;
|
||||
monitorLayoutChanged();
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: CompositorService.isHyprland ? Hyprland.monitors : null
|
||||
delegate: Connections {
|
||||
required property HyprlandMonitor modelData
|
||||
target: modelData
|
||||
function onLastIpcObjectChanged() {
|
||||
root._syncMonitorLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setLayoutXray(enabled) {
|
||||
layoutXrayEnabled = enabled;
|
||||
_layoutXrayLoaded = true;
|
||||
|
||||
@@ -148,14 +148,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Wakes monitors powered off by the "power off monitors on lock" path.
|
||||
// Lock.qml's own wake handlers sit outside the session-lock surface and
|
||||
// never receive input, so wake on input via seat-level idle-notify instead.
|
||||
IdleMonitor {
|
||||
id: lockWakeMonitor
|
||||
timeout: 1
|
||||
respectInhibitors: false
|
||||
enabled: root.enabled && root.isShellLocked && root.monitorsOff && (SettingsData.lockScreenPowerOffMonitorsOnLock || root.lockPowerOffRequested)
|
||||
enabled: root.enabled && root.isShellLocked && root.monitorsOff
|
||||
onIsIdleChanged: {
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
@@ -67,6 +67,17 @@ Singleton {
|
||||
root.allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx);
|
||||
}
|
||||
|
||||
function updateWorkspace(workspaceId, changes) {
|
||||
const ws = root.workspaces[workspaceId];
|
||||
if (!ws)
|
||||
return;
|
||||
if (Object.keys(changes).every(key => ws[key] === changes[key]))
|
||||
return;
|
||||
const updatedWorkspaces = Object.assign({}, root.workspaces);
|
||||
updatedWorkspaces[workspaceId] = Object.assign({}, ws, changes);
|
||||
setWorkspaces(updatedWorkspaces);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
validateProcess.running = true;
|
||||
}
|
||||
@@ -512,40 +523,20 @@ Singleton {
|
||||
if (changed)
|
||||
windows = updatedWindows;
|
||||
|
||||
if (focusedWindow) {
|
||||
const ws = root.workspaces[focusedWindow.workspace_id];
|
||||
if (ws && ws.active_window_id !== focusedWindowId) {
|
||||
const updatedWs = {};
|
||||
for (let prop in ws) {
|
||||
updatedWs[prop] = ws[prop];
|
||||
}
|
||||
updatedWs.active_window_id = focusedWindowId;
|
||||
|
||||
const updatedWorkspaces = {};
|
||||
for (const id in root.workspaces) {
|
||||
updatedWorkspaces[id] = id === focusedWindow.workspace_id ? updatedWs : root.workspaces[id];
|
||||
}
|
||||
setWorkspaces(updatedWorkspaces);
|
||||
}
|
||||
}
|
||||
if (!focusedWindow)
|
||||
return;
|
||||
updateWorkspace(focusedWindow.workspace_id, {
|
||||
"active_window_id": focusedWindowId
|
||||
});
|
||||
}
|
||||
|
||||
function handleWorkspaceActiveWindowChanged(data) {
|
||||
const ws = root.workspaces[data.workspace_id];
|
||||
if (ws) {
|
||||
const updatedWs = {};
|
||||
for (let prop in ws) {
|
||||
updatedWs[prop] = ws[prop];
|
||||
}
|
||||
updatedWs.active_window_id = data.active_window_id;
|
||||
if (root.workspaces[data.workspace_id]) {
|
||||
if (data.active_window_id !== null && data.active_window_id !== undefined)
|
||||
lastFocusedWindowId = data.active_window_id;
|
||||
|
||||
const updatedWorkspaces = {};
|
||||
for (const id in root.workspaces) {
|
||||
updatedWorkspaces[id] = id === data.workspace_id ? updatedWs : root.workspaces[id];
|
||||
}
|
||||
setWorkspaces(updatedWorkspaces);
|
||||
updateWorkspace(data.workspace_id, {
|
||||
"active_window_id": data.active_window_id
|
||||
});
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
@@ -682,20 +673,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function handleWorkspaceUrgencyChanged(data) {
|
||||
const ws = root.workspaces[data.id];
|
||||
if (!ws)
|
||||
if (!root.workspaces[data.id])
|
||||
return;
|
||||
const updatedWs = {};
|
||||
for (let prop in ws) {
|
||||
updatedWs[prop] = ws[prop];
|
||||
}
|
||||
updatedWs.is_urgent = data.urgent;
|
||||
|
||||
const updatedWorkspaces = {};
|
||||
for (const id in root.workspaces) {
|
||||
updatedWorkspaces[id] = id === data.id ? updatedWs : root.workspaces[id];
|
||||
}
|
||||
setWorkspaces(updatedWorkspaces);
|
||||
updateWorkspace(data.id, {
|
||||
"is_urgent": data.urgent
|
||||
});
|
||||
|
||||
windowUrgentChanged();
|
||||
}
|
||||
|
||||
@@ -504,6 +504,7 @@ Singleton {
|
||||
};
|
||||
|
||||
policy.bypassDnd = rules.some(rule => rule.bypassDnd === true && _matchesNotificationRule(rule, info));
|
||||
policy.disablePopup = rules.some(rule => (rule.action || "default").toString().toLowerCase() === "mute" && _matchesNotificationRule(rule, info));
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!_matchesNotificationRule(rule, info))
|
||||
@@ -514,9 +515,6 @@ Singleton {
|
||||
case "ignore":
|
||||
policy.drop = true;
|
||||
break;
|
||||
case "mute":
|
||||
policy.disablePopup = true;
|
||||
break;
|
||||
case "popup_only":
|
||||
policy.hideFromCenter = true;
|
||||
policy.disableHistory = true;
|
||||
@@ -699,23 +697,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the freedesktop "suppress-sound" hint: the sender
|
||||
// plays its own audio for this notification and asks the
|
||||
// server not to double up. "sound-name" is the opposite — an
|
||||
// explicit request for audio — so it plays even when the
|
||||
// global new-notification sound is off.
|
||||
const soundHints = notif.hints || {};
|
||||
const suppressSound = !!soundHints["suppress-sound"];
|
||||
const requestsSound = !!soundHints["sound-name"];
|
||||
const dndBlocked = SessionData.doNotDisturb && !_allowedInDnd(policy.urgency, policy.bypassDnd);
|
||||
if (!dndBlocked && SettingsData.soundsEnabled && (SettingsData.soundNewNotification || requestsSound) && !suppressSound) {
|
||||
if (policy.urgency === NotificationUrgency.Critical) {
|
||||
AudioService.playCriticalNotificationSound();
|
||||
} else {
|
||||
AudioService.playNormalNotificationSound();
|
||||
}
|
||||
}
|
||||
|
||||
const shouldShowPopup = !root.popupsDisabled && !dndBlocked && !policy.disablePopup;
|
||||
const isTransient = notif.transient;
|
||||
const shouldKeepInCenter = !isTransient && !policy.hideFromCenter;
|
||||
@@ -727,6 +709,22 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
// Honor the freedesktop "suppress-sound" hint: the sender
|
||||
// plays its own audio for this notification and asks the
|
||||
// server not to double up. "sound-name" is the opposite — an
|
||||
// explicit request for audio — so it plays even when the
|
||||
// global new-notification sound is off.
|
||||
const soundHints = notif.hints || {};
|
||||
const suppressSound = !!soundHints["suppress-sound"];
|
||||
const requestsSound = !!soundHints["sound-name"];
|
||||
if (!dndBlocked && !policy.disablePopup && SettingsData.soundsEnabled && (SettingsData.soundNewNotification || requestsSound) && !suppressSound) {
|
||||
if (policy.urgency === NotificationUrgency.Critical) {
|
||||
AudioService.playCriticalNotificationSound();
|
||||
} else {
|
||||
AudioService.playNormalNotificationSound();
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = notifComponent.createObject(root, {
|
||||
"popup": shouldShowPopup,
|
||||
"notification": notif,
|
||||
|
||||
@@ -45,7 +45,7 @@ Singleton {
|
||||
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready) {
|
||||
if (!node || !node.ready || node.properties?.["media.role"] === "Screen") {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -42,11 +42,14 @@ Singleton {
|
||||
signal sessionLocked
|
||||
signal sessionUnlocked
|
||||
signal sessionResumed
|
||||
signal lidOpened
|
||||
signal loginctlStateChanged
|
||||
|
||||
property bool stateInitialized: false
|
||||
property string prepareForSleepSubscriptionId: ""
|
||||
property bool prepareForSleepSubscriptionPending: false
|
||||
property string lidSubscriptionId: ""
|
||||
property bool lidSubscriptionPending: false
|
||||
property double lastResumeSignalTimestamp: 0
|
||||
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
@@ -696,7 +699,7 @@ Singleton {
|
||||
if (DMSService.isConnected) {
|
||||
checkDMSCapabilities();
|
||||
} else {
|
||||
clearPrepareForSleepSubscriptionState();
|
||||
clearPowerSubscriptionState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,10 +717,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function onDbusSignalReceived(subscriptionId, data) {
|
||||
if (subscriptionId !== prepareForSleepSubscriptionId) {
|
||||
return;
|
||||
if (subscriptionId === prepareForSleepSubscriptionId) {
|
||||
handlePrepareForSleepSignal(data);
|
||||
} else if (subscriptionId === lidSubscriptionId) {
|
||||
handleLidPropertiesChanged(data);
|
||||
}
|
||||
handlePrepareForSleepSignal(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,14 +783,41 @@ Singleton {
|
||||
|
||||
if (DMSService.capabilities.includes("dbus")) {
|
||||
ensurePrepareForSleepSubscription();
|
||||
ensureLidSubscription();
|
||||
} else {
|
||||
clearPrepareForSleepSubscriptionState();
|
||||
clearPowerSubscriptionState();
|
||||
}
|
||||
}
|
||||
|
||||
function clearPrepareForSleepSubscriptionState() {
|
||||
function clearPowerSubscriptionState() {
|
||||
prepareForSleepSubscriptionId = "";
|
||||
prepareForSleepSubscriptionPending = false;
|
||||
lidSubscriptionId = "";
|
||||
lidSubscriptionPending = false;
|
||||
}
|
||||
|
||||
function ensureLidSubscription() {
|
||||
if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus"))
|
||||
return;
|
||||
if (lidSubscriptionId || lidSubscriptionPending)
|
||||
return;
|
||||
|
||||
lidSubscriptionPending = true;
|
||||
DMSService.dbusSubscribe("system", "org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.DBus.Properties", "PropertiesChanged", response => {
|
||||
lidSubscriptionPending = false;
|
||||
if (response.error) {
|
||||
log.warn("Failed to subscribe to lid changes:", response.error);
|
||||
return;
|
||||
}
|
||||
lidSubscriptionId = response.result?.subscriptionId || "";
|
||||
});
|
||||
}
|
||||
|
||||
function handleLidPropertiesChanged(data) {
|
||||
if (data?.path !== "/org/freedesktop/UPower" || data.body?.[0] !== "org.freedesktop.UPower")
|
||||
return;
|
||||
if (data.body?.[1]?.LidIsClosed === false)
|
||||
lidOpened();
|
||||
}
|
||||
|
||||
function ensurePrepareForSleepSubscription() {
|
||||
|
||||
@@ -207,6 +207,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function isMine(peer) {
|
||||
if (!peer)
|
||||
return false;
|
||||
if (selfNode && (peer === selfNode || peer.id === selfNode.id))
|
||||
return true;
|
||||
const myOwner = selfNode ? (selfNode.owner || "") : "";
|
||||
if (peer.owner === myOwner && myOwner !== "")
|
||||
return true;
|
||||
|
||||
@@ -37,11 +37,6 @@ layout(std140, binding = 0) uniform buf {
|
||||
vec4 chromeParam3;
|
||||
} ubuf;
|
||||
|
||||
float sdBox(vec2 p, vec2 c, vec2 hs) {
|
||||
vec2 q = abs(p - c) - hs;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0)));
|
||||
}
|
||||
|
||||
float sdRoundBox(vec2 p, vec2 c, vec2 hs, float r) {
|
||||
r = min(r, min(hs.x, hs.y));
|
||||
vec2 q = abs(p - c) - hs + r;
|
||||
@@ -73,12 +68,9 @@ float chromeK(vec2 px, vec4 rect, vec4 ks) {
|
||||
}
|
||||
|
||||
float sceneDist(vec2 px) {
|
||||
vec2 sc = vec2(ubuf.widthPx, ubuf.heightPx) * 0.5;
|
||||
float dOuter = sdBox(px, sc, sc);
|
||||
vec2 cutC = vec2((ubuf.cutout.x + ubuf.cutout.z) * 0.5, (ubuf.cutout.y + ubuf.cutout.w) * 0.5);
|
||||
vec2 cutH = vec2((ubuf.cutout.z - ubuf.cutout.x) * 0.5, (ubuf.cutout.w - ubuf.cutout.y) * 0.5);
|
||||
float dCut = sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius);
|
||||
float d = max(dOuter, -dCut);
|
||||
float d = -sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius);
|
||||
|
||||
if (ubuf.chromeParam0.x > 0.5)
|
||||
d = smin(d, chromeDist(px, ubuf.chromeRect0, ubuf.chromeCorner0), chromeK(px, ubuf.chromeRect0, ubuf.chromeK0));
|
||||
|
||||
@@ -15,11 +15,6 @@ layout(std140, binding = 0) uniform buf {
|
||||
vec4 surfaceColor; // straight (non-premultiplied) rgba
|
||||
} ubuf;
|
||||
|
||||
float sdBox(vec2 p, vec2 c, vec2 hs) {
|
||||
vec2 q = abs(p - c) - hs;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0)));
|
||||
}
|
||||
|
||||
float sdRoundBox(vec2 p, vec2 c, vec2 hs, float r) {
|
||||
r = min(r, min(hs.x, hs.y));
|
||||
vec2 q = abs(p - c) - hs + r;
|
||||
@@ -28,12 +23,9 @@ float sdRoundBox(vec2 p, vec2 c, vec2 hs, float r) {
|
||||
|
||||
void main() {
|
||||
vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx);
|
||||
vec2 sc = vec2(ubuf.widthPx, ubuf.heightPx) * 0.5;
|
||||
float dOuter = sdBox(px, sc, sc);
|
||||
vec2 cutC = vec2((ubuf.cutout.x + ubuf.cutout.z) * 0.5, (ubuf.cutout.y + ubuf.cutout.w) * 0.5);
|
||||
vec2 cutH = vec2((ubuf.cutout.z - ubuf.cutout.x) * 0.5, (ubuf.cutout.w - ubuf.cutout.y) * 0.5);
|
||||
float dCut = sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius);
|
||||
float d = max(dOuter, -dCut);
|
||||
float d = -sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius);
|
||||
|
||||
float fw = max(fwidth(d), 1e-4);
|
||||
float cov = 1.0 - smoothstep(-fw, fw, d);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
v1.6.1
|
||||
v1.6.3
|
||||
|
||||
@@ -68,8 +68,8 @@ Item {
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
wrapMode: Text.NoWrap
|
||||
width: root.needsScrolling ? implicitWidth : root.width
|
||||
elide: root.needsScrolling ? Text.ElideNone : Text.ElideRight
|
||||
width: Math.min(implicitWidth, root.width)
|
||||
elide: SettingsData.scrollTitleEnabled ? Text.ElideNone : Text.ElideRight
|
||||
x: Math.round((root.needsScrolling ? -Math.min(root.scrollOffset, root.maxScrollOffset) : 0) + root.textShift)
|
||||
opacity: 1
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#%PAM-1.0
|
||||
|
||||
auth required pam_fprintd.so max-tries=5
|
||||
# timeout bounds how long a stuck fprintd session can hold the reader.
|
||||
# Modules/Lock/Pam.qml mirrors the value in sessionTimeoutMs to tell an expiry
|
||||
# apart from a device fault; change both together.
|
||||
auth required pam_fprintd.so max-tries=1 timeout=90
|
||||
account required pam_permit.so
|
||||
password required pam_deny.so
|
||||
session required pam_permit.so
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
contrast=4
|
||||
|
||||
[General]
|
||||
ColorScheme=DankMatugen
|
||||
Name=Dank Shell (matugen)
|
||||
ColorScheme=DankMatugenDark
|
||||
Name=Dank Shell (matugen) Dark
|
||||
|
||||
[ColorEffects:Disabled]
|
||||
Color={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
Color={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ColorAmount=0
|
||||
ColorEffect=0
|
||||
ContrastAmount=0.65
|
||||
@@ -16,7 +16,7 @@ IntensityEffect=2
|
||||
|
||||
[ColorEffects:Inactive]
|
||||
ChangeSelectionColor=true
|
||||
Color={{colors.outline.default.red}},{{colors.outline.default.green}},{{colors.outline.default.blue}}
|
||||
Color={{colors.outline.dark.red}},{{colors.outline.dark.green}},{{colors.outline.dark.blue}}
|
||||
ColorAmount=0.025
|
||||
ColorEffect=2
|
||||
ContrastAmount=0.1
|
||||
@@ -26,121 +26,121 @@ IntensityAmount=0
|
||||
IntensityEffect=0
|
||||
|
||||
[Colors:Button]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface.dark.red}},{{colors.surface.dark.green}},{{colors.surface.dark.blue}}
|
||||
BackgroundNormal={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:Complementary]
|
||||
BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface_container.dark.red}},{{colors.surface_container.dark.green}},{{colors.surface_container.dark.blue}}
|
||||
BackgroundNormal={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:Header]
|
||||
BackgroundAlternate={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
BackgroundNormal={{colors.surface.dark.red}},{{colors.surface.dark.green}},{{colors.surface.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:Header][Inactive]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface.dark.red}},{{colors.surface.dark.green}},{{colors.surface.dark.blue}}
|
||||
BackgroundNormal={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:Selection]
|
||||
BackgroundAlternate={{colors.primary_container.default.red}},{{colors.primary_container.default.green}},{{colors.primary_container.default.blue}}
|
||||
BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.primary_container.dark.red}},{{colors.primary_container.dark.green}},{{colors.primary_container.dark.blue}}
|
||||
BackgroundNormal={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.on_primary.dark.red}},{{colors.on_primary.dark.green}},{{colors.on_primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_primary.dark.red}},{{colors.on_primary.dark.green}},{{colors.on_primary.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:Tooltip]
|
||||
BackgroundAlternate={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
BackgroundNormal={{colors.surface.dark.red}},{{colors.surface.dark.green}},{{colors.surface.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:View]
|
||||
BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface_container_low.dark.red}},{{colors.surface_container_low.dark.green}},{{colors.surface_container_low.dark.blue}}
|
||||
BackgroundNormal={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[Colors:Window]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface.dark.red}},{{colors.surface.dark.green}},{{colors.surface.dark.blue}}
|
||||
BackgroundNormal={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
DecorationFocus={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
DecorationHover={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundActive={{colors.primary.dark.red}},{{colors.primary.dark.green}},{{colors.primary.dark.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
ForegroundLink={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundNegative={{colors.error.dark.red}},{{colors.error.dark.green}},{{colors.error.dark.blue}}
|
||||
ForegroundNeutral={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
ForegroundNormal={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
ForegroundPositive={{colors.tertiary.dark.red}},{{colors.tertiary.dark.green}},{{colors.tertiary.dark.blue}}
|
||||
ForegroundVisited={{colors.secondary.dark.red}},{{colors.secondary.dark.green}},{{colors.secondary.dark.blue}}
|
||||
|
||||
[WM]
|
||||
activeBackground={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
activeBlend={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
activeForeground={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
inactiveBackground={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
inactiveBlend={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
inactiveForeground={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
activeBackground={{colors.surface.dark.red}},{{colors.surface.dark.green}},{{colors.surface.dark.blue}}
|
||||
activeBlend={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
activeForeground={{colors.on_surface.dark.red}},{{colors.on_surface.dark.green}},{{colors.on_surface.dark.blue}}
|
||||
inactiveBackground={{colors.background.dark.red}},{{colors.background.dark.green}},{{colors.background.dark.blue}}
|
||||
inactiveBlend={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
inactiveForeground={{colors.on_surface_variant.dark.red}},{{colors.on_surface_variant.dark.green}},{{colors.on_surface_variant.dark.blue}}
|
||||
|
||||
@@ -28,9 +28,9 @@ IntensityEffect=0
|
||||
[Colors:Button]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
@@ -42,9 +42,9 @@ ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.gr
|
||||
[Colors:Complementary]
|
||||
BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
@@ -56,9 +56,9 @@ ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.gr
|
||||
[Colors:Header]
|
||||
BackgroundAlternate={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
@@ -70,9 +70,9 @@ ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.gr
|
||||
[Colors:Header][Inactive]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
@@ -82,25 +82,25 @@ ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.gre
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
|
||||
[Colors:Selection]
|
||||
BackgroundAlternate={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
BackgroundNormal={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
BackgroundAlternate={{colors.primary_container.default.red}},{{colors.primary_container.default.green}},{{colors.primary_container.default.blue}}
|
||||
BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundNormal={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
|
||||
[Colors:Tooltip]
|
||||
BackgroundAlternate={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
@@ -112,9 +112,9 @@ ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.gr
|
||||
[Colors:View]
|
||||
BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
@@ -126,9 +126,9 @@ ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.gr
|
||||
[Colors:Window]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
DecorationHover={{colors.inverse_primary.default.red}},{{colors.inverse_primary.default.green}},{{colors.inverse_primary.default.blue}}
|
||||
ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cursor {{colors.on_surface.default.hex}}
|
||||
cursor_text_color {{colors.on_surface_variant.default.hex}}
|
||||
cursor {{colors.primary.default.hex}}
|
||||
cursor_text_color {{colors.on_primary.default.hex}}
|
||||
|
||||
foreground {{colors.on_surface.default.hex}}
|
||||
background {{colors.background.default.hex}}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
contrast=4
|
||||
|
||||
[General]
|
||||
ColorScheme=DankMatugen
|
||||
Name=Dank Shell (matugen)
|
||||
ColorScheme=DankMatugenLight
|
||||
Name=Dank Shell (matugen) Light
|
||||
|
||||
[ColorEffects:Disabled]
|
||||
Color={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
Color={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ColorAmount=0
|
||||
ColorEffect=0
|
||||
ContrastAmount=0.65
|
||||
@@ -16,7 +16,7 @@ IntensityEffect=2
|
||||
|
||||
[ColorEffects:Inactive]
|
||||
ChangeSelectionColor=true
|
||||
Color={{colors.outline.default.red}},{{colors.outline.default.green}},{{colors.outline.default.blue}}
|
||||
Color={{colors.outline.light.red}},{{colors.outline.light.green}},{{colors.outline.light.blue}}
|
||||
ColorAmount=0.025
|
||||
ColorEffect=2
|
||||
ContrastAmount=0.1
|
||||
@@ -26,121 +26,121 @@ IntensityAmount=0
|
||||
IntensityEffect=0
|
||||
|
||||
[Colors:Button]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface.light.red}},{{colors.surface.light.green}},{{colors.surface.light.blue}}
|
||||
BackgroundNormal={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:Complementary]
|
||||
BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface_container.light.red}},{{colors.surface_container.light.green}},{{colors.surface_container.light.blue}}
|
||||
BackgroundNormal={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:Header]
|
||||
BackgroundAlternate={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
BackgroundNormal={{colors.surface.light.red}},{{colors.surface.light.green}},{{colors.surface.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:Header][Inactive]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface.light.red}},{{colors.surface.light.green}},{{colors.surface.light.blue}}
|
||||
BackgroundNormal={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:Selection]
|
||||
BackgroundAlternate={{colors.primary_container.default.red}},{{colors.primary_container.default.green}},{{colors.primary_container.default.blue}}
|
||||
BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_primary.default.red}},{{colors.on_primary.default.green}},{{colors.on_primary.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.primary_container.light.red}},{{colors.primary_container.light.green}},{{colors.primary_container.light.blue}}
|
||||
BackgroundNormal={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.on_primary.light.red}},{{colors.on_primary.light.green}},{{colors.on_primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_primary.light.red}},{{colors.on_primary.light.green}},{{colors.on_primary.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:Tooltip]
|
||||
BackgroundAlternate={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
BackgroundNormal={{colors.surface.light.red}},{{colors.surface.light.green}},{{colors.surface.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:View]
|
||||
BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface_container_low.light.red}},{{colors.surface_container_low.light.green}},{{colors.surface_container_low.light.blue}}
|
||||
BackgroundNormal={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[Colors:Window]
|
||||
BackgroundAlternate={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
BackgroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
ForegroundLink={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}}
|
||||
ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}}
|
||||
ForegroundVisited={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}}
|
||||
BackgroundAlternate={{colors.surface.light.red}},{{colors.surface.light.green}},{{colors.surface.light.blue}}
|
||||
BackgroundNormal={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
DecorationFocus={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
DecorationHover={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundActive={{colors.primary.light.red}},{{colors.primary.light.green}},{{colors.primary.light.blue}}
|
||||
ForegroundInactive={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
ForegroundLink={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundNegative={{colors.error.light.red}},{{colors.error.light.green}},{{colors.error.light.blue}}
|
||||
ForegroundNeutral={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
ForegroundNormal={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
ForegroundPositive={{colors.tertiary.light.red}},{{colors.tertiary.light.green}},{{colors.tertiary.light.blue}}
|
||||
ForegroundVisited={{colors.secondary.light.red}},{{colors.secondary.light.green}},{{colors.secondary.light.blue}}
|
||||
|
||||
[WM]
|
||||
activeBackground={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}}
|
||||
activeBlend={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
activeForeground={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}}
|
||||
inactiveBackground={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}}
|
||||
inactiveBlend={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
inactiveForeground={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}}
|
||||
activeBackground={{colors.surface.light.red}},{{colors.surface.light.green}},{{colors.surface.light.blue}}
|
||||
activeBlend={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
activeForeground={{colors.on_surface.light.red}},{{colors.on_surface.light.green}},{{colors.on_surface.light.blue}}
|
||||
inactiveBackground={{colors.background.light.red}},{{colors.background.light.green}},{{colors.background.light.blue}}
|
||||
inactiveBlend={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
inactiveForeground={{colors.on_surface_variant.light.red}},{{colors.on_surface_variant.light.green}},{{colors.on_surface_variant.light.blue}}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
--text-brand: {{colors.primary.default.hex}} !important;
|
||||
--control-brand-foreground: {{colors.primary.default.hex}} !important;
|
||||
--control-brand-foreground-new: {{colors.primary.default.hex}} !important;
|
||||
--badge-text-brand: {{colors.primary.default.hex}} !important;
|
||||
--badge-text-brand: {{colors.surface.default.hex}} !important;
|
||||
|
||||
--text-danger: {{colors.error.default.hex}} !important;
|
||||
--text-feedback-critical: {{colors.error.default.hex}} !important;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"list.focusOutline": "{{colors.primary.dark.hex}}",
|
||||
"list.focusBackground": "{{colors.surface_container_high.dark.hex}}",
|
||||
"list.highlightForeground": "{{colors.primary.dark.hex}}",
|
||||
"list.focusHighlightForeground": "{{colors.primary.dark.hex}}",
|
||||
"list.focusHighlightForeground": "{{colors.on_primary.dark.hex}}",
|
||||
"list.errorForeground": "{{colors.error.dark.hex}}",
|
||||
"list.warningForeground": "{{colors.secondary.dark.hex}}",
|
||||
//
|
||||
@@ -82,13 +82,16 @@
|
||||
"dropdown.border": "{{colors.outline_variant.dark.hex}}",
|
||||
"quickInput.background": "{{colors.surface_container_low.dark.hex}}",
|
||||
"quickInput.foreground": "{{colors.on_surface.dark.hex}}",
|
||||
"quickInputList.focusBackground": "{{colors.primary.dark.hex}}",
|
||||
"quickInputList.focusForeground": "{{colors.on_primary.dark.hex}}",
|
||||
"quickInputList.focusIconForeground": "{{colors.on_primary.dark.hex}}",
|
||||
"widget.shadow": "{{colors.background.dark.hex}}80",
|
||||
//
|
||||
// Buttons
|
||||
//
|
||||
"button.background": "{{colors.primary.dark.hex}}",
|
||||
"button.foreground": "{{colors.on_primary.dark.hex}}",
|
||||
"button.hoverBackground": "{{colors.primary_container.dark.hex}}",
|
||||
"button.hoverBackground": "{{colors.primary_fixed.dark.hex}}",
|
||||
//
|
||||
// Status bar
|
||||
//
|
||||
@@ -158,9 +161,10 @@
|
||||
"editorHoverWidget.border": "{{colors.outline.dark.hex}}",
|
||||
"editorSuggestWidget.background": "{{colors.surface_container.dark.hex}}",
|
||||
"editorSuggestWidget.foreground": "{{colors.on_surface.dark.hex}}",
|
||||
"editorSuggestWidget.selectedBackground": "{{colors.surface_container_high.dark.hex}}",
|
||||
"editorSuggestWidget.selectedForeground": "{{colors.on_surface.dark.hex}}",
|
||||
"editorSuggestWidget.selectedBackground": "{{colors.primary.dark.hex}}",
|
||||
"editorSuggestWidget.selectedForeground": "{{colors.on_primary.dark.hex}}",
|
||||
"editorSuggestWidget.highlightForeground": "{{colors.primary.dark.hex}}",
|
||||
"editorSuggestWidget.focusHighlightForeground": "{{colors.on_primary.dark.hex}}",
|
||||
//
|
||||
// Scrollbar
|
||||
//
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"list.focusOutline": "{{colors.primary.default.hex}}",
|
||||
"list.focusBackground": "{{colors.surface_container_high.default.hex}}",
|
||||
"list.highlightForeground": "{{colors.primary.default.hex}}",
|
||||
"list.focusHighlightForeground": "{{colors.primary.default.hex}}",
|
||||
"list.focusHighlightForeground": "{{colors.on_primary.default.hex}}",
|
||||
"list.errorForeground": "{{colors.error.default.hex}}",
|
||||
"list.warningForeground": "{{colors.secondary.default.hex}}",
|
||||
"input.background": "{{colors.surface_container_low.default.hex}}",
|
||||
@@ -61,10 +61,13 @@
|
||||
"dropdown.border": "{{colors.outline_variant.default.hex}}",
|
||||
"quickInput.background": "{{colors.surface_container_low.default.hex}}",
|
||||
"quickInput.foreground": "{{colors.on_surface.default.hex}}",
|
||||
"quickInputList.focusBackground": "{{colors.primary.default.hex}}",
|
||||
"quickInputList.focusForeground": "{{colors.on_primary.default.hex}}",
|
||||
"quickInputList.focusIconForeground": "{{colors.on_primary.default.hex}}",
|
||||
"widget.shadow": "{{colors.background.default.hex}}80",
|
||||
"button.background": "{{colors.primary.default.hex}}",
|
||||
"button.foreground": "{{colors.on_primary.default.hex}}",
|
||||
"button.hoverBackground": "{{colors.primary_container.default.hex}}",
|
||||
"button.hoverBackground": "{{colors.primary_fixed.default.hex}}",
|
||||
"statusBar.background": "{{colors.background.default.hex}}",
|
||||
"statusBar.foreground": "{{colors.on_surface.default.hex}}",
|
||||
"statusBar.border": "{{colors.outline_variant.default.hex}}",
|
||||
@@ -113,9 +116,10 @@
|
||||
"editorHoverWidget.border": "{{colors.outline.default.hex}}",
|
||||
"editorSuggestWidget.background": "{{colors.surface_container.default.hex}}",
|
||||
"editorSuggestWidget.foreground": "{{colors.on_surface.default.hex}}",
|
||||
"editorSuggestWidget.selectedBackground": "{{colors.surface_container_high.default.hex}}",
|
||||
"editorSuggestWidget.selectedForeground": "{{colors.on_surface.default.hex}}",
|
||||
"editorSuggestWidget.selectedBackground": "{{colors.primary.default.hex}}",
|
||||
"editorSuggestWidget.selectedForeground": "{{colors.on_primary.default.hex}}",
|
||||
"editorSuggestWidget.highlightForeground": "{{colors.primary.default.hex}}",
|
||||
"editorSuggestWidget.focusHighlightForeground": "{{colors.on_primary.default.hex}}",
|
||||
"scrollbarSlider.background": "{{colors.on_surface_variant.default.hex}}50",
|
||||
"scrollbarSlider.hoverBackground": "{{colors.on_surface_variant.default.hex}}80",
|
||||
"scrollbarSlider.activeBackground": "{{colors.on_surface_variant.default.hex}}AA",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user