mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-22 11:14:01 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -532,7 +532,7 @@ jobs:
|
||||
- dgop
|
||||
- dankgo
|
||||
# - dank-qml-common
|
||||
- dankcalendar
|
||||
# - dankcalendar
|
||||
- danksearch
|
||||
- dank-greeter
|
||||
steps:
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-nswS/ygp0oYMXp6YHbbIxBha4xvzjKAxcAS5yOpbslg=";
|
||||
vendorHash = "sha256-ejDi+TzstUtxtWpcl5yYw4UZuTnZraOsIdJuXPKxJ6s=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -34,6 +34,10 @@ PluginComponent {
|
||||
TailscaleService.connectTailscale(null);
|
||||
}
|
||||
|
||||
onCcWidgetExpanded: {
|
||||
TailscaleService.refresh(null);
|
||||
}
|
||||
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
id: detailRoot
|
||||
|
||||
@@ -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
|
||||
@@ -2028,7 +2029,7 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
@@ -2038,9 +2039,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 +2067,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 +2087,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.2
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,6 +22,8 @@ apply_qt_colors() {
|
||||
local config_file="$1"
|
||||
|
||||
if [ -f "$config_file" ]; then
|
||||
sed -i 's/^\[Appearance\]\\ncustom_palette=.*/[Appearance]/' "$config_file"
|
||||
|
||||
if grep -q '^\[Appearance\]' "$config_file"; then
|
||||
if grep -q '^custom_palette=' "$config_file"; then
|
||||
sed -i 's/^custom_palette=.*/custom_palette=true/' "$config_file"
|
||||
@@ -43,7 +45,7 @@ apply_qt_colors() {
|
||||
} >>"$config_file"
|
||||
fi
|
||||
else
|
||||
printf '[Appearance]\\ncustom_palette=true\\ncolor_scheme_path=%s\\n' "$color_scheme_path" >"$config_file"
|
||||
printf '[Appearance]\ncustom_palette=true\ncolor_scheme_path=%s\n' "$color_scheme_path" >"$config_file"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user