1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-08 06:28:27 -04:00

Compare commits

...

13 Commits

Author SHA1 Message Date
Huỳnh Thiện Lộc f971b6bcff feat(theme): add extract dynamic theme to JSON file (#3009)
* feat(theme): add extract current theme to JSON file

Adds an 'Extract' button in Settings > Theme Color that exports the
current theme (generic/dynamic/custom/registry) to a dual-mode dark+light
JSON file via the DMS file save dialog.

* refactor(theme): simplify extract button to single DankButton

* refactor(theme): move extract button inside dynamic theme section

* fix(theme): address review feedback on extract theme save

* refactor: utilize dankActionButton

---------

(cherry picked from commit bde4decc5d)
2026-08-08 04:32:15 +00:00
purian23 0313e96e8e fix(doctor): correctly report built-in font status
Port 1.5

(cherry picked from commit 3c817dc0d0)
2026-08-07 23:47:49 -04:00
bbedward c38b161f01 fix(geolocation): fetch IP location lazily so startup never contacts ip-api.com
fixes #3008
port 1.5

(cherry picked from commit a713485599)
2026-08-07 23:55:18 +00:00
bbedward acd82589e2 fix(displays): auto-select restores named profiles matching the connected output set
fixes #3012
port 1.5

(cherry picked from commit f16bdda969)
2026-08-07 22:57:31 +00:00
purian23 170721a1f1 fix(notifWidget): restore hand cursor on hover
Port 1.5

(cherry picked from commit a575363f89)
2026-08-07 03:45:00 +00:00
Huỳnh Thiện Lộc 7e0af52d5b fix(launcher): honor source badge setting (#3002)
(cherry picked from commit 7cb97f4462)
2026-08-06 00:42:38 -04:00
Huỳnh Thiện Lộc e1b6427929 fix(settings): hide removed widget tooltip (#3003)
(cherry picked from commit 82b1b36b1a)
2026-08-06 00:42:38 -04:00
Huỳnh Thiện Lộc 47bf6a8dfd fix(slideout): yield keyboard focus when modal is open on same screen (#2902)
DankSlideout always held WlrKeyboardFocus.OnDemand regardless of modals,
causing Notepad to steal focus back when Launcher was opened.

ModalManager: clone currentModalsByScreen on mutation so QML bindings
re-evaluate when modals open/close.
DankSlideout: set keyboardFocus to None while a modal is active on the
same screen, letting the modal (launcher) claim focus exclusively.

(cherry picked from commit 9981fcf529)
2026-08-06 00:42:38 -04:00
Thomas Kroll 84ac36295f fix(tray): don't dismiss click-opened tray menus from hover controller (#2979)
closeHoverSurfaces() called TrayMenuManager.closeAllMenus(), tearing down
every registered tray menu including ones opened by right-click. With
hoverPopouts enabled, moving the pointer after a right-click re-ran the
hit test and closed the menu before it could be reached.

Tag each menu with how it was opened and add closeHoverMenus(), which
only closes hover-opened menus. The hover controller now uses that.

Click-driven callers pass no argument, so byHover stays undefined and
`byHover === true` evaluates to false.

Fixes #2978

Claude-Session: https://claude.ai/code/session_01DYF9vdsaToU4Usu7kxPCNB

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 34626070af)
2026-08-06 00:42:38 -04:00
jack-mil ca839487de fix(mpris): remove required non-null artist/title from idle condition (#2993)
The current isIdle detection only works if the player has a title _and_ artist metadata,
which is not appropriate for all cases, e.g. MPV playing a video does not set artist.

In the current implementation, after a video stops playing in MPV, and the window closes
(but MPV and the MPRIS plugin server is still running as a background process), the playback controls remain
shown on the bar, and have non-functional controls.

It is sufficient to only check for the "Stopped" playing state. The MPRIS specification
states that Playback_Status==Stopped should be used to indicate that *NO* track is currently
playing. A compliant player should set that, and should also set the CanPlay/CanPause boolean
to false is not track is being played.

See: https://specifications.freedesktop.org/mpris/latest/Player_Interface.html#Enum:Playback_Status
(cherry picked from commit 49f968d26b)
2026-08-06 00:42:38 -04:00
bbedward dabbec1dce keybinds: fix labeling of spawn keybinds
fixes #2957

(cherry picked from commit 5bb884db57)
2026-08-06 00:42:37 -04:00
euletheia bd4d35d215 fix(BatteryService): fix typo in notification check (#2944)
(cherry picked from commit 8594a414ec)
2026-08-06 00:42:37 -04:00
euletheia cfdacd3ea3 fix(BatteryService): fix unresolved icons for notifications and dismiss alerts when plugged in (#2943)
* feat(BatteryService): dismiss sticky critical battery notifications

If critical notifications do not timeout, dismiss the battery-related
ones automatically when battery is plugged in.

* fix(BatteryService): use material icons for battery alerts

Since the previously specified icons are not resolved, instead reuse
icons from DMS first-party plugin for critical battery alerts.

(cherry picked from commit df396bfa43)
2026-08-06 00:42:37 -04:00
21 changed files with 433 additions and 164 deletions
+164 -86
View File
@@ -449,12 +449,7 @@ func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
func checkDMSInstallation() []checkResult { func checkDMSInstallation() []checkResult {
var results []checkResult var results []checkResult
dmsPath := "" dmsPath := resolveDoctorShellPath()
if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = configPath
} else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path
}
if dmsPath == "" { if dmsPath == "" {
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}} return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
@@ -1167,99 +1162,182 @@ func formatResultsPlain(results []checkResult) string {
return sb.String() return sb.String()
} }
const (
defaultDoctorFontFamily = "Inter Variable"
defaultDoctorMonoFontFamily = "Fira Code"
)
// bundledFontRelPaths maps settings/default family names to font files shipped with
// the shell and loaded via Qt FontLoader (not registered with fontconfig).
var bundledFontRelPaths = map[string][]string{
"inter variable": {
"DankCommon/assets/fonts/inter/InterVariable.ttf",
"assets/fonts/inter/InterVariable.ttf",
},
"fira code": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
},
"firacode nerd font": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
},
}
func resolveDoctorShellPath() string {
if err := findConfig(nil, nil); err == nil && configPath != "" {
return configPath
}
if path, err := config.LocateDMSConfig(); err == nil {
return path
}
return ""
}
func findBundledFontFile(shellPath, family string) string {
if shellPath == "" {
return ""
}
relPaths, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
if !ok {
return ""
}
for _, rel := range relPaths {
path := filepath.Join(shellPath, rel)
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path
}
}
return ""
}
func isBundledDefaultFont(family string) bool {
_, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
return ok
}
func fontInFcList(name, cacheLower string) bool {
target := strings.ToLower(strings.TrimSpace(name))
if target == "" {
return false
}
for _, line := range strings.Split(cacheLower, "\n") {
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
}
}
return false
}
func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailable bool, url string) checkResult {
if bundled := findBundledFontFile(shellPath, family); bundled != "" {
details := "Bundled (Qt FontLoader)"
if doctorVerbose {
details = bundled
}
return checkResult{catFonts, label, statusOK, family, details, url}
}
if isBundledDefaultFont(family) {
if shellPath == "" {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not verified", family),
"Could not locate shell config to verify bundled font files.",
url,
}
}
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' bundled file missing", family),
"Expected font file missing from shell install. Reinstall DMS or check DankCommon assets.",
url,
}
}
if !fcListAvailable {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not verified", family),
"fc-list not installed; cannot verify custom fonts in fontconfig.",
url,
}
}
if fcCache == "" {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not found", family),
"Fontconfig cache is empty or unreadable. Try running 'fc-cache -fv'.",
url,
}
}
if fontInFcList(family, fcCache) {
return checkResult{catFonts, label, statusOK, family, "Available via fontconfig", url}
}
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not found", family),
"Font is not registered with fontconfig. Try running 'fc-cache -fv' or install the font.",
url,
}
}
func checkFonts() []checkResult { func checkFonts() []checkResult {
var results []checkResult var results []checkResult
url := doctorDocsURL + "#fonts" url := doctorDocsURL + "#fonts"
configDir, err := os.UserConfigDir() fontFamily := defaultDoctorFontFamily
if err != nil { monoFontFamily := defaultDoctorMonoFontFamily
return nil
}
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
fontFamily := "Inter Variable" if configDir, err := os.UserConfigDir(); err == nil {
monoFontFamily := "Fira Code" settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
if data, err := os.ReadFile(settingsPath); err == nil {
if data, err := os.ReadFile(settingsPath); err == nil { var settings struct {
var settings struct { FontFamily string `json:"fontFamily"`
FontFamily string `json:"fontFamily"` MonoFontFamily string `json:"monoFontFamily"`
MonoFontFamily string `json:"monoFontFamily"`
}
if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" {
fontFamily = settings.FontFamily
} }
if settings.MonoFontFamily != "" { if err := json.Unmarshal(data, &settings); err == nil {
monoFontFamily = settings.MonoFontFamily if settings.FontFamily != "" {
} fontFamily = settings.FontFamily
} }
} if settings.MonoFontFamily != "" {
monoFontFamily = settings.MonoFontFamily
if !utils.CommandExists("fc-list") {
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify if fonts are cached.", url})
return results
}
// Retrieve font list
output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
return results
}
outStr := string(output)
if len(strings.TrimSpace(outStr)) == 0 {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
return results
}
lowerFonts := strings.ToLower(outStr)
// Helper to check if a font exists
hasFont := func(name string) bool {
target := strings.ToLower(strings.TrimSpace(name))
if target == "" {
return false
}
for _, line := range strings.Split(lowerFonts, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Each line can have comma-separated families
families := strings.Split(line, ",")
for _, fam := range families {
if strings.TrimSpace(fam) == target {
return true
} }
} }
} }
return false
} }
// Normal Font Check shellPath := resolveDoctorShellPath()
if hasFont(fontFamily) { needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily)
results = append(results, checkResult{catFonts, "Normal Font", statusOK, fontFamily, "Available", url})
} else { fcListAvailable := utils.CommandExists("fc-list")
results = append(results, checkResult{ fcCache := ""
catFonts, "Normal Font", statusWarn,
fmt.Sprintf("'%s' not found", fontFamily), if needFontconfig {
"Font is not registered. Try running 'fc-cache -fv' or install the font.", if !fcListAvailable {
url, results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify custom fonts in fontconfig cache.", url})
}) } else {
output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
} else {
fcCache = strings.ToLower(string(output))
if len(strings.TrimSpace(fcCache)) == 0 {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
}
}
}
} }
// Monospace Font Check results = append(results,
if hasFont(monoFontFamily) { checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url),
results = append(results, checkResult{catFonts, "Monospace Font", statusOK, monoFontFamily, "Available", url}) checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url),
} else { )
results = append(results, checkResult{
catFonts, "Monospace Font", statusWarn,
fmt.Sprintf("'%s' not found", monoFontFamily),
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
url,
})
}
return results return results
} }
+1 -31
View File
@@ -6,37 +6,7 @@ func NewClient() Client {
geoclueClient, err := newGeoClueClient() geoclueClient, err := newGeoClueClient()
if err != nil { if err != nil {
log.Warnf("GeoClue2 unavailable: %v", err) log.Warnf("GeoClue2 unavailable: %v", err)
return newSeededIpClient() return newIpClient()
} }
loc, _ := geoclueClient.GetLocation()
if loc.Latitude != 0 || loc.Longitude != 0 {
log.Info("Using GeoClue2 location")
return geoclueClient
}
log.Info("GeoClue2 has no fix yet, seeding with IP location")
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location seed failed: %v", err)
return geoclueClient
}
log.Info("Seeded GeoClue2 with IP location")
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
return geoclueClient return geoclueClient
} }
func newSeededIpClient() *IpClient {
client := newIpClient()
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location also failed: %v", err)
return client
}
log.Info("Using IP location")
client.currLocation.Latitude = ipLoc.Latitude
client.currLocation.Longitude = ipLoc.Longitude
return client
}
+22 -6
View File
@@ -34,6 +34,7 @@ const (
type GeoClueClient struct { type GeoClueClient struct {
currLocation *Location currLocation *Location
locationMutex sync.RWMutex locationMutex sync.RWMutex
seedOnce sync.Once
dbusConn *dbus.Conn dbusConn *dbus.Conn
clientPath dbus.ObjectPath clientPath dbus.ObjectPath
@@ -230,14 +231,29 @@ func (c *GeoClueClient) SeedLocation(loc Location) {
} }
func (c *GeoClueClient) GetLocation() (Location, error) { func (c *GeoClueClient) GetLocation() (Location, error) {
loc := c.currentLocation()
if loc.Latitude != 0 || loc.Longitude != 0 {
return loc, nil
}
c.seedOnce.Do(func() {
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("GeoClue2 has no fix, IP location seed failed: %v", err)
return
}
log.Info("Seeded GeoClue2 with IP location")
c.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
})
return c.currentLocation(), nil
}
func (c *GeoClueClient) currentLocation() Location {
c.locationMutex.RLock() c.locationMutex.RLock()
defer c.locationMutex.RUnlock() defer c.locationMutex.RUnlock()
if c.currLocation == nil { if c.currLocation == nil {
return Location{ return Location{}
Latitude: 0.0,
Longitude: 0.0,
}, nil
} }
stateCopy := *c.currLocation return *c.currLocation
return stateCopy, nil
} }
+20 -17
View File
@@ -8,24 +8,11 @@ import (
) )
func NewManager(client geolocation.Client) (*Manager, error) { func NewManager(client geolocation.Client) (*Manager, error) {
currLocation, err := client.GetLocation()
if err != nil {
log.Warnf("Failed to get initial location: %v", err)
}
m := &Manager{ m := &Manager{
client: client, client: client,
dirty: make(chan struct{}), dirty: make(chan struct{}),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
state: &State{},
state: &State{
Latitude: currLocation.Latitude,
Longitude: currLocation.Longitude,
},
}
if err := m.startSignalPump(); err != nil {
return nil, err
} }
m.notifierWg.Add(1) m.notifierWg.Add(1)
@@ -34,6 +21,22 @@ func NewManager(client geolocation.Client) (*Manager, error) {
return m, nil return m, nil
} }
// The geolocation client may fetch IP location on first use, so nothing
// touches it until a consumer actually asks for location data.
func (m *Manager) ensureStarted() {
m.startOnce.Do(func() {
go func() {
currLocation, err := m.client.GetLocation()
if err != nil {
log.Warnf("Failed to get initial location: %v", err)
} else {
m.handleLocationChange(currLocation)
}
m.startSignalPump()
}()
})
}
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.notifierWg.Wait() m.notifierWg.Wait()
@@ -48,6 +51,7 @@ func (m *Manager) Close() {
} }
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
m.ensureStarted()
ch := make(chan State, 64) ch := make(chan State, 64)
m.subscribers.Store(id, ch) m.subscribers.Store(id, ch)
return ch return ch
@@ -59,7 +63,7 @@ func (m *Manager) Unsubscribe(id string) {
} }
} }
func (m *Manager) startSignalPump() error { func (m *Manager) startSignalPump() {
m.sigWG.Add(1) m.sigWG.Add(1)
go func() { go func() {
defer m.sigWG.Done() defer m.sigWG.Done()
@@ -80,8 +84,6 @@ func (m *Manager) startSignalPump() error {
} }
} }
}() }()
return nil
} }
func (m *Manager) handleLocationChange(location geolocation.Location) { func (m *Manager) handleLocationChange(location geolocation.Location) {
@@ -102,6 +104,7 @@ func (m *Manager) notifySubscribers() {
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
m.ensureStarted()
m.stateMutex.RLock() m.stateMutex.RLock()
defer m.stateMutex.RUnlock() defer m.stateMutex.RUnlock()
if m.state == nil { if m.state == nil {
+2 -1
View File
@@ -16,7 +16,8 @@ type Manager struct {
state *State state *State
stateMutex sync.RWMutex stateMutex sync.RWMutex
client geolocation.Client client geolocation.Client
startOnce sync.Once
stopChan chan struct{} stopChan chan struct{}
sigWG sync.WaitGroup sigWG sync.WaitGroup
+11 -2
View File
@@ -15,7 +15,11 @@ Singleton {
function openModal(modal) { function openModal(modal) {
PopoutManager.screenshotActive = false; PopoutManager.screenshotActive = false;
const screenName = modal.effectiveScreen?.name ?? "unknown"; const screenName = modal.effectiveScreen?.name ?? "unknown";
currentModalsByScreen[screenName] = modal; var next = {};
for (var k in currentModalsByScreen)
next[k] = currentModalsByScreen[k];
next[screenName] = modal;
currentModalsByScreen = next;
modalChanged(); modalChanged();
Qt.callLater(() => { Qt.callLater(() => {
if (!modal.allowStacking) if (!modal.allowStacking)
@@ -34,7 +38,12 @@ Singleton {
function closeModal(modal) { function closeModal(modal) {
const screenName = modal.effectiveScreen?.name ?? "unknown"; const screenName = modal.effectiveScreen?.name ?? "unknown";
if (currentModalsByScreen[screenName] === modal) { if (currentModalsByScreen[screenName] === modal) {
delete currentModalsByScreen[screenName]; var next = {};
for (var k in currentModalsByScreen) {
if (k !== screenName)
next[k] = currentModalsByScreen[k];
}
currentModalsByScreen = next;
modalChanged(); modalChanged();
} }
} }
+80
View File
@@ -436,6 +436,10 @@ Singleton {
function getMatugenColor(path, fallback) { function getMatugenColor(path, fallback) {
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"; const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
return getMatugenColorForMode(colorMode, path, fallback);
}
function getMatugenColorForMode(colorMode, path, fallback) {
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]; let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
for (const part of path.split(".")) { for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur)) if (!cur || typeof cur !== "object" || !(part in cur))
@@ -445,6 +449,82 @@ Singleton {
return cur || fallback; return cur || fallback;
} }
function extractCurrentTheme(themeName) {
var name = themeName || "Extracted Theme";
var dark = {};
var light = {};
if (currentTheme === dynamic) {
dark = buildExtractedDynamicMode("dark", name + " Dark");
light = buildExtractedDynamicMode("light", name + " Light");
} else if (currentTheme === custom && customThemeRawData) {
var rawDark = customThemeRawData.dark || null;
var rawLight = customThemeRawData.light || null;
if (rawDark) {
dark = JSON.parse(JSON.stringify(rawDark));
if (!dark.name)
dark.name = name + " Dark";
} else if (rawLight) {
dark = buildExtractedDynamicMode("dark", name + " Dark");
} else {
dark = currentThemeData ? JSON.parse(JSON.stringify(currentThemeData)) : {};
dark.name = name + " Dark";
}
if (rawLight) {
light = JSON.parse(JSON.stringify(rawLight));
if (!light.name)
light.name = name + " Light";
} else if (rawDark) {
light = buildExtractedDynamicMode("light", name + " Light");
} else {
light = currentThemeData ? JSON.parse(JSON.stringify(currentThemeData)) : {};
light.name = name + " Light";
}
} else {
var darkTheme = StockThemes.getThemeByName(currentTheme, false);
var lightTheme = StockThemes.getThemeByName(currentTheme, true);
dark = darkTheme ? JSON.parse(JSON.stringify(darkTheme)) : {};
light = lightTheme ? JSON.parse(JSON.stringify(lightTheme)) : {};
dark.name = name + " Dark";
light.name = name + " Light";
}
return JSON.stringify({
dark: dark,
light: light
}, null, 2);
}
function buildExtractedDynamicMode(colorMode, name) {
return {
"name": name,
"primary": getMatugenColorForMode(colorMode, "primary", "#42a5f5"),
"primaryText": getMatugenColorForMode(colorMode, "on_primary", "#ffffff"),
"primaryContainer": getMatugenColorForMode(colorMode, "primary_container", "#1976d2"),
"secondary": getMatugenColorForMode(colorMode, "secondary", "#8ab4f8"),
"secondaryContainer": getMatugenColorForMode(colorMode, "secondary_container", getMatugenColorForMode(colorMode, "surface_container_high", "#292b2f")),
"tertiary": getMatugenColorForMode(colorMode, "tertiary", "#efb8c8"),
"tertiaryContainer": getMatugenColorForMode(colorMode, "tertiary_container", getMatugenColorForMode(colorMode, "surface_container_high", "#292b2f")),
"surface": getMatugenColorForMode(colorMode, "surface", "#1a1c1e"),
"surfaceText": getMatugenColorForMode(colorMode, "on_background", "#e3e8ef"),
"surfaceVariant": getMatugenColorForMode(colorMode, "surface_variant", "#44464f"),
"surfaceVariantText": getMatugenColorForMode(colorMode, "on_surface_variant", "#c4c7c5"),
"surfaceTint": getMatugenColorForMode(colorMode, "surface_tint", "#8ab4f8"),
"background": getMatugenColorForMode(colorMode, "background", "#1a1c1e"),
"backgroundText": getMatugenColorForMode(colorMode, "on_background", "#e3e8ef"),
"outline": getMatugenColorForMode(colorMode, "outline", "#8e918f"),
"surfaceContainerLowest": getMatugenColorForMode(colorMode, "surface_container_lowest", "#0e1013"),
"surfaceContainerLow": getMatugenColorForMode(colorMode, "surface_container_low", "#181a1d"),
"surfaceContainer": getMatugenColorForMode(colorMode, "surface_container", "#1e2023"),
"surfaceContainerHigh": getMatugenColorForMode(colorMode, "surface_container_high", "#292b2f"),
"surfaceContainerHighest": getMatugenColorForMode(colorMode, "surface_container_highest", "#343740"),
"error": getMatugenColorForMode(colorMode, "error", "#F2B8B5"),
"warning": "#FF9800",
"info": "#2196F3",
"success": "#4CAF50"
};
}
readonly property var currentThemeData: { readonly property var currentThemeData: {
if (currentTheme === "custom") { if (currentTheme === "custom") {
return customThemeData || StockThemes.getThemeByName("purple", isLightMode); return customThemeData || StockThemes.getThemeByName("purple", isLightMode);
+12
View File
@@ -22,6 +22,18 @@ Singleton {
activeTrayMenus = newMenus activeTrayMenus = newMenus
} }
function closeHoverMenus() {
for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName]
if (!menu || menu.openedByHover !== true) continue
if (typeof menu.close === "function") {
menu.close()
} else if (menu.showMenu !== undefined) {
menu.showMenu = false
}
}
}
function closeAllMenus() { function closeAllMenus() {
for (const screenName in activeTrayMenus) { for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName] const menu = activeTrayMenus[screenName]
@@ -7,6 +7,7 @@ Item {
property string source: "" property string source: ""
property int glyphSize: 14 property int glyphSize: 14
property bool badgeVisible: true
readonly property var sourceAsset: ({ readonly property var sourceAsset: ({
"flatpak": "../../assets/package-sources/flatpak.svg", "flatpak": "../../assets/package-sources/flatpak.svg",
@@ -17,7 +18,7 @@ Item {
readonly property string assetPath: sourceAsset[source] || "" readonly property string assetPath: sourceAsset[source] || ""
visible: SettingsData.dankLauncherV2ShowSourceBadges && assetPath.length > 0 visible: badgeVisible && SettingsData.dankLauncherV2ShowSourceBadges && assetPath.length > 0
implicitWidth: glyphSize implicitWidth: glyphSize
implicitHeight: glyphSize implicitHeight: glyphSize
@@ -178,7 +178,7 @@ Rectangle {
anchors.margins: Theme.spacingXS anchors.margins: Theme.spacingXS
source: root.item?.type === "app" ? (root.item.source || "") : "" source: root.item?.type === "app" ? (root.item.source || "") : ""
glyphSize: 16 glyphSize: 16
visible: !root.isSelected && !!source badgeVisible: !root.isSelected
} }
} }
} }
@@ -609,7 +609,7 @@ Item {
_closeHoverNotepad(); _closeHoverNotepad();
activeHoverTrigger = ""; activeHoverTrigger = "";
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen); PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
TrayMenuManager.closeAllMenus(); TrayMenuManager.closeHoverMenus();
} }
function _beginSupersededCloseForActive() { function _beginSupersededCloseForActive() {
@@ -67,6 +67,7 @@ BasePill {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.MiddleButton acceptedButtons: Qt.MiddleButton
cursorShape: Qt.PointingHandCursor
onPressed: mouse => { onPressed: mouse => {
root.triggerRipple(this, mouse.x, mouse.y); root.triggerRipple(this, mouse.x, mouse.y);
SessionData.setDoNotDisturb(!SessionData.doNotDisturb); SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
@@ -1484,6 +1484,7 @@ BasePill {
property bool isVertical: false property bool isVertical: false
property var axis: null property var axis: null
property bool showMenu: false property bool showMenu: false
property bool openedByHover: false
property var menuHandle: null property var menuHandle: null
ListModel { ListModel {
@@ -1493,7 +1494,8 @@ BasePill {
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null; return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
openedByHover = byHover === true;
trayItem = item; trayItem = item;
anchorItem = anchor; anchorItem = anchor;
parentScreen = screen; parentScreen = screen;
@@ -2084,7 +2086,7 @@ BasePill {
} }
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
if (!screen) if (!screen)
return; return;
if (currentTrayMenu) { if (currentTrayMenu) {
@@ -2099,7 +2101,7 @@ BasePill {
currentTrayMenu = trayMenuComponent.createObject(null); currentTrayMenu = trayMenuComponent.createObject(null);
if (!currentTrayMenu) if (!currentTrayMenu)
return; return;
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj); currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj, byHover === true);
} }
function _trayLayoutRoot() { function _trayLayoutRoot() {
@@ -2147,7 +2149,7 @@ BasePill {
if (!hit?.trayItem?.hasMenu) if (!hit?.trayItem?.hasMenu)
return false; return false;
const anchor = hit.children?.length > 0 ? hit.children[0] : hit; const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis); showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis, true);
return true; return true;
} }
} }
@@ -302,17 +302,22 @@ Singleton {
function findConfigEntryByFingerprint(data, outputIdentifiers, autoOnly) { function findConfigEntryByFingerprint(data, outputIdentifiers, autoOnly) {
const targetKey = outputSetFingerprint(outputIdentifiers); const targetKey = outputSetFingerprint(outputIdentifiers);
const configs = data.configurations || []; const configs = data.configurations || [];
let firstUnnamed = null;
for (let i = 0; i < configs.length; i++) { for (let i = 0; i < configs.length; i++) {
if (configFingerprint(configs[i]) === targetKey) { if (configFingerprint(configs[i]) !== targetKey)
if (autoOnly && configs[i].name) continue;
continue; if (configs[i].name && !autoOnly)
return { return {
entry: configs[i], entry: configs[i],
index: i index: i
}; };
} if (!configs[i].name && !firstUnnamed)
firstUnnamed = {
entry: configs[i],
index: i
};
} }
return null; return firstUnnamed;
} }
function getProfileMonitorInclusion(profileId) { function getProfileMonitorInclusion(profileId) {
@@ -751,7 +756,7 @@ Singleton {
return; return;
readMonitorsJson(data => { readMonitorsJson(data => {
const match = findConfigEntryByFingerprint(data, currentOutputSet, true); const match = findConfigEntryByFingerprint(data, currentOutputSet, false);
if (match) { if (match) {
if (configEntryMatchesLiveLayout(match.entry)) { if (configEntryMatchesLiveLayout(match.entry)) {
SettingsData.setActiveDisplayProfile(CompositorService.compositor, match.entry.id); SettingsData.setActiveDisplayProfile(CompositorService.compositor, match.entry.id);
+64 -1
View File
@@ -1,6 +1,7 @@
import QtCore import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
@@ -13,6 +14,7 @@ Item {
id: themeColorsTab id: themeColorsTab
property var parentModal: null property var parentModal: null
property string pendingExtractJson: ""
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
readonly property bool frameModeActive: SettingsData.frameEnabled readonly property bool frameModeActive: SettingsData.frameEnabled
property var cachedIconThemes: SettingsData.availableIconThemes property var cachedIconThemes: SettingsData.availableIconThemes
@@ -564,7 +566,7 @@ Item {
} }
Column { Column {
width: parent.width - 120 - Theme.spacingM width: parent.width - 120 - Theme.spacingM - 36 - Theme.spacingM
spacing: Theme.spacingS spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -586,6 +588,7 @@ Item {
} }
StyledText { StyledText {
id: wallpaperPathText
text: { text: {
if (ToastService.wallpaperErrorStatus === "error") if (ToastService.wallpaperErrorStatus === "error")
return I18n.tr("Wallpaper processing failed", "wallpaper processing error"); return I18n.tr("Wallpaper processing failed", "wallpaper processing error");
@@ -603,6 +606,22 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
DankActionButton {
buttonSize: 36
iconName: "download"
iconSize: Theme.iconSize
backgroundColor: Theme.primaryHover
iconColor: Theme.primary
tooltipText: I18n.tr("Extract theme to JSON", "extract theme tooltip")
anchors.bottom: wallpaperPathText.bottom
onClicked: {
pendingExtractJson = Theme.extractCurrentTheme();
saveBrowserLoader.active = true;
if (saveBrowserLoader.item)
saveBrowserLoader.item.open();
}
}
} }
SettingsDropdownRow { SettingsDropdownRow {
@@ -2977,6 +2996,28 @@ Item {
} }
} }
LazyLoader {
id: saveBrowserLoader
active: false
FileBrowserSurfaceModal {
id: saveBrowser
browserTitle: I18n.tr("Save Extracted Theme", "extract theme save dialog title")
browserIcon: "download"
browserType: "default"
fileExtensions: ["*.json"]
allowStacking: true
saveMode: true
defaultFileName: "dms-extracted-theme.json"
onFileSelected: path => {
saveExtractedTheme(pendingExtractJson, Paths.strip(path));
close();
}
}
}
LazyLoader { LazyLoader {
id: themeBrowserLoader id: themeBrowserLoader
active: false active: false
@@ -2992,4 +3033,26 @@ Item {
if (themeBrowserLoader.item) if (themeBrowserLoader.item)
themeBrowserLoader.item.show(); themeBrowserLoader.item.show();
} }
FileView {
id: extractSaveFileView
blockWrites: true
preload: false
atomicWrites: true
printErrors: true
onSaved: {
ToastService.showInfo(I18n.tr("Theme extracted to: %1", "extract theme success").arg(Paths.strip(extractSaveFileView.path)));
}
onSaveFailed: error => {
ToastService.showError(I18n.tr("Failed to extract theme", "extract theme error"));
log.warn("Failed to write extracted theme to " + extractSaveFileView.path + ": " + error);
}
}
function saveExtractedTheme(json, outputPath) {
extractSaveFileView.path = outputPath;
extractSaveFileView.setText(json);
}
} }
@@ -18,6 +18,8 @@ Column {
id: sharedTooltip id: sharedTooltip
} }
Component.onDestruction: sharedTooltip.hide()
signal itemEnabledChanged(string sectionId, string itemId, bool enabled) signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
signal itemOrderChanged(string sectionId, var orderedIds) signal itemOrderChanged(string sectionId, var orderedIds)
signal addWidget(string sectionId) signal addWidget(string sectionId)
@@ -1215,6 +1217,7 @@ Column {
iconSize: 18 iconSize: 18
iconColor: Theme.error iconColor: Theme.error
onClicked: { onClicked: {
sharedTooltip.hide();
root.removeWidget(root.sectionId, index); root.removeWidget(root.sectionId, index);
} }
onEntered: { onEntered: {
+25 -4
View File
@@ -143,7 +143,7 @@ Singleton {
function sendAlert(title, message, isWarning, category, notificationType) { function sendAlert(title, message, isWarning, category, notificationType) {
if (notificationType === 1) { if (notificationType === 1) {
Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "battery-caution" : "battery-charging", title, message]); Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "material:battery_alert" : "material:battery_charging_full", title, message]);
} else { } else {
if (isWarning) { if (isWarning) {
ToastService.showWarning(title, message, "", category); ToastService.showWarning(title, message, "", category);
@@ -157,7 +157,7 @@ Singleton {
if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) { if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) {
if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) { if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) {
_hasNotifiedChargeLimit = true; _hasNotifiedChargeLimit = true;
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "battery-charge-limit", SettingsData.batteryChargeLimitNotificationType); sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "material:battery_profile", SettingsData.batteryChargeLimitNotificationType);
} }
} else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) { } else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) {
_hasNotifiedChargeLimit = false; _hasNotifiedChargeLimit = false;
@@ -173,7 +173,7 @@ Singleton {
if (isCriticalBattery) { if (isCriticalBattery) {
if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) { if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) {
_hasNotifiedCriticalBattery = true; _hasNotifiedCriticalBattery = true;
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "battery-critical", SettingsData.batteryCriticalNotificationType); sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "material:battery_alert", SettingsData.batteryCriticalNotificationType);
} }
return; return;
} }
@@ -186,7 +186,7 @@ Singleton {
if (isLowBattery) { if (isLowBattery) {
if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) { if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) {
_hasNotifiedLowBattery = true; _hasNotifiedLowBattery = true;
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "battery-low", SettingsData.batteryLowNotificationType); sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "material:battery_0_bar", SettingsData.batteryLowNotificationType);
} }
if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) { if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) {
@@ -230,6 +230,27 @@ Singleton {
applyPowerProfile(); applyPowerProfile();
if (isPluggedIn) {
const dismissLow = SettingsData.batteryLowNotificationType === 1 && SettingsData.notificationTimeoutNormal === 0;
const dismissCritical = SettingsData.batteryCriticalNotificationType === 1 && SettingsData.notificationTimeoutCritical === 0;
if (dismissLow || dismissCritical) {
const lowSummary = I18n.tr("Low Battery");
const criticalSummary = I18n.tr("Critical Battery");
for (const w of NotificationService.visibleNotifications) {
if (!w || !w.notification)
continue;
const summary = w.notification.summary;
if ((dismissLow && summary === lowSummary) || (dismissCritical && summary === criticalSummary)) {
NotificationService.dismissNotification(w);
}
}
}
}
previousPluggedState = isPluggedIn; previousPluggedState = isPluggedIn;
} }
+2
View File
@@ -388,6 +388,8 @@ Singleton {
const binds = bindsData[cat]; const binds = bindsData[cat];
for (var i = 0; i < binds.length; i++) { for (var i = 0; i < binds.length; i++) {
const bind = binds[i]; const bind = binds[i];
if (currentProvider === "hyprland" && bind.action && bind.action.startsWith("exec "))
bind.action = "spawn " + bind.action.slice(5);
const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat; const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat;
if (!processed[targetCat]) if (!processed[targetCat])
processed[targetCat] = []; processed[targetCat] = [];
+1 -1
View File
@@ -168,7 +168,7 @@ Singleton {
} }
function isIdle(player: MprisPlayer): bool { function isIdle(player: MprisPlayer): bool {
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist; return player && player.playbackState === MprisPlaybackState.Stopped;
} }
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title // Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
+3 -1
View File
@@ -105,9 +105,11 @@ PanelWindow {
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
readonly property string _slideoutScreenName: modelData?.name ?? ""
WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: 0 WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: isVisible && !ModalManager.currentModalsByScreen[_slideoutScreenName] ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
readonly property real dpr: CompositorService.getScreenScale(root.screen) readonly property real dpr: CompositorService.getScreenScale(root.screen)
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr) readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
+1 -1
View File
@@ -869,7 +869,7 @@ Item {
readonly property var tooltipTexts: ({ readonly property var tooltipTexts: ({
"dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"), "dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
"compositor": I18n.tr("Niri compositor actions (focus, move, etc.)"), "compositor": I18n.tr("Compositor actions (focus, move, etc.)", "keybind action type tooltip"),
"spawn": I18n.tr("Run a program (e.g., firefox, kitty)"), "spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
"shell": I18n.tr("Run a shell command (e.g., notify-send)") "shell": I18n.tr("Run a shell command (e.g., notify-send)")
}) })