1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Compare commits

..

15 Commits

Author SHA1 Message Date
purian23
80025804ab theme: Tweaks to Auto Color Mode 2026-01-24 20:43:45 -05:00
bbedward
028d3b4e61 workspaces: fix index numbers with show apps on vBar + animation 2026-01-24 20:31:45 -05:00
purian23
9cce5ccfe6 autoThemeMode: Add transition time & layout update 2026-01-24 19:33:37 -05:00
purian23
a260b8060e Merge branch 'master' into auto-theme 2026-01-24 18:19:13 -05:00
purian23
f945307232 cleanup: Auto theme switcher 2026-01-24 17:48:34 -05:00
bbedward
8f44d52cb2 launcher v2: allow categories in plugins 2026-01-24 16:58:55 -05:00
purian23
3413cb7b89 feat: Create new Auto theme mode based on region / time of day 2026-01-24 16:38:45 -05:00
bbedward
4e3b24ffbb settings: migrate vpnLastConnected to session
fixes #1488
2026-01-24 16:08:15 -05:00
bbedward
03cfa55e0b ipc: ass toast IPCs
fixes #964
2026-01-24 12:53:51 -05:00
bbedward
a887e60f40 keybinds: fix MangoWC config traversal in provider
fixes #1464
2026-01-24 12:23:59 -05:00
bbedward
816819bf9f dankinstall: fix xero color typo 2026-01-23 23:10:24 -05:00
bbedward
78f3bb3812 dankinstall: support XeroLinux
fixes #1474
2026-01-23 22:39:14 -05:00
bbedward
01d7ed5dd8 launcher v2: ability to toggle visibility in modal 2026-01-23 22:17:35 -05:00
Lucas
50311db280 nix: add qt-imageformats to DMS qml dependencies (#1479)
* nix: add qt-imageformats to DMS qml dependencies

* nix: update flake.lock
2026-01-23 21:53:35 -05:00
bbedward
01b1a276c5 launcher v2: support ScreenCopy in tiles 2026-01-23 21:29:48 -05:00
33 changed files with 3020 additions and 797 deletions

View File

@@ -41,6 +41,9 @@ func init() {
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
}
type ArchDistribution struct {

View File

@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
p.dmsProcessed = true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
expanded, err := utils.ExpandPath(sourcePath)
if err != nil {
return
}
includedBinds, err := p.parseFileWithSource(expanded)
fullPath := expanded
if !filepath.IsAbs(expanded) {
fullPath = filepath.Join(baseDir, expanded)
}
includedBinds, err := p.parseFileWithSource(fullPath)
if err != nil {
return
}
@@ -521,33 +521,10 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
}
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
data, err := os.ReadFile(dmsBindsPath)
keybinds, err := p.parseFileWithSource(dmsBindsPath)
if err != nil {
return nil
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
p.dmsProcessed = true
return keybinds
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -44,6 +45,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
if strings.HasPrefix(req.Method, "theme.auto.") {
if themeModeManager == nil {
models.RespondError(conn, req.ID, "theme mode manager not initialized")
return
}
thememode.HandleRequest(conn, req, themeModeManager)
return
}
if strings.HasPrefix(req.Method, "loginctl.") {
if loginctlManager == nil {
models.RespondError(conn, req.ID, "loginctl manager not initialized")

View File

@@ -28,6 +28,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -68,6 +69,7 @@ var evdevManager *evdev.Manager
var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
const dbusClientID = "dms-dbus-client"
@@ -380,6 +382,14 @@ func InitializeDbusManager() error {
return nil
}
func InitializeThemeModeManager() error {
manager := thememode.NewManager()
themeModeManager = manager
log.Info("Theme mode automation manager initialized")
return nil
}
func handleConnection(conn net.Conn) {
defer conn.Close()
@@ -457,6 +467,10 @@ func getCapabilities() Capabilities {
caps = append(caps, "clipboard")
}
if themeModeManager != nil {
caps = append(caps, "theme.auto")
}
if dbusManager != nil {
caps = append(caps, "dbus")
}
@@ -519,6 +533,10 @@ func getServerInfo() ServerInfo {
caps = append(caps, "clipboard")
}
if themeModeManager != nil {
caps = append(caps, "theme.auto")
}
if dbusManager != nil {
caps = append(caps, "dbus")
}
@@ -791,6 +809,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}()
}
if shouldSubscribe("theme.auto") && themeModeManager != nil {
wg.Add(1)
themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto")
go func() {
defer wg.Done()
defer themeModeManager.Unsubscribe(clientID + "-theme-auto")
initialState := themeModeManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-themeAutoChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("bluetooth") && bluezManager != nil {
wg.Add(1)
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
@@ -1251,6 +1301,9 @@ func cleanupManagers() {
if dbusManager != nil {
dbusManager.Close()
}
if themeModeManager != nil {
themeModeManager.Close()
}
if wlContext != nil {
wlContext.Close()
}
@@ -1346,6 +1399,15 @@ func Start(printDocs bool) error {
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
log.Info("Theme automation:")
log.Info(" theme.auto.getState - Get current theme automation state")
log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)")
log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])")
log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)")
log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)")
log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)")
log.Info(" theme.auto.trigger - Trigger immediate re-evaluation")
log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)")
log.Info("Bluetooth:")
log.Info(" bluetooth.getState - Get current bluetooth state")
log.Info(" bluetooth.startDiscovery - Start device discovery")
@@ -1503,6 +1565,12 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err)
}
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil {
go func() {

View File

@@ -0,0 +1,154 @@
package thememode
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "theme mode manager not initialized")
return
}
switch req.Method {
case "theme.auto.getState":
handleGetState(conn, req, manager)
case "theme.auto.setEnabled":
handleSetEnabled(conn, req, manager)
case "theme.auto.setMode":
handleSetMode(conn, req, manager)
case "theme.auto.setSchedule":
handleSetSchedule(conn, req, manager)
case "theme.auto.setLocation":
handleSetLocation(conn, req, manager)
case "theme.auto.setUseIPLocation":
handleSetUseIPLocation(conn, req, manager)
case "theme.auto.trigger":
handleTrigger(conn, req, manager)
case "theme.auto.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetEnabled(enabled)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
}
func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
mode, err := params.String(req.Params, "mode")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if mode != "time" && mode != "location" {
models.RespondError(conn, req.ID, "invalid mode")
return
}
manager.SetMode(mode)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
}
func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
startHour, err := params.Int(req.Params, "startHour")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
startMinute, err := params.Int(req.Params, "startMinute")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
endHour, err := params.Int(req.Params, "endHour")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
endMinute, err := params.Int(req.Params, "endMinute")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetSchedule(startHour, startMinute, endHour, endMinute)
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
lat, err := params.Float(req.Params, "latitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
lon, err := params.Float(req.Params, "longitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetLocation(lat, lon)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
}
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
use, err := params.Bool(req.Params, "use")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetUseIPLocation(use)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
}
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
manager.TriggerUpdate()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range stateChan {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
Result: &state,
}); err != nil {
return
}
}
}

View File

@@ -0,0 +1,432 @@
package thememode
import (
"errors"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const (
defaultStartHour = 18
defaultStartMinute = 0
defaultEndHour = 6
defaultEndMinute = 0
defaultElevationTwilight = -6.0
defaultElevationDaylight = 3.0
)
type Manager struct {
config Config
configMutex sync.RWMutex
state *State
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
locationMutex sync.RWMutex
cachedIPLat *float64
cachedIPLon *float64
stopChan chan struct{}
updateTrigger chan struct{}
wg sync.WaitGroup
}
func NewManager() *Manager {
m := &Manager{
config: Config{
Enabled: false,
Mode: "time",
StartHour: defaultStartHour,
StartMinute: defaultStartMinute,
EndHour: defaultEndHour,
EndMinute: defaultEndMinute,
ElevationTwilight: defaultElevationTwilight,
ElevationDaylight: defaultElevationDaylight,
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
}
m.updateState(time.Now())
m.wg.Add(1)
go m.schedulerLoop()
return m
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{Config: m.getConfig()}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) SetEnabled(enabled bool) {
m.configMutex.Lock()
if m.config.Enabled == enabled {
m.configMutex.Unlock()
return
}
m.config.Enabled = enabled
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetMode(mode string) {
m.configMutex.Lock()
if m.config.Mode == mode {
m.configMutex.Unlock()
return
}
m.config.Mode = mode
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
m.configMutex.Lock()
changed := m.config.StartHour != startHour ||
m.config.StartMinute != startMinute ||
m.config.EndHour != endHour ||
m.config.EndMinute != endMinute
if !changed {
m.configMutex.Unlock()
return
}
m.config.StartHour = startHour
m.config.StartMinute = startMinute
m.config.EndHour = endHour
m.config.EndMinute = endMinute
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetLocation(lat, lon float64) {
m.configMutex.Lock()
if m.config.Latitude != nil && m.config.Longitude != nil &&
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
m.configMutex.Unlock()
return
}
m.config.Latitude = &lat
m.config.Longitude = &lon
m.config.UseIPLocation = false
m.configMutex.Unlock()
m.locationMutex.Lock()
m.cachedIPLat = nil
m.cachedIPLon = nil
m.locationMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetUseIPLocation(use bool) {
m.configMutex.Lock()
if m.config.UseIPLocation == use {
m.configMutex.Unlock()
return
}
m.config.UseIPLocation = use
if use {
m.config.Latitude = nil
m.config.Longitude = nil
}
m.configMutex.Unlock()
if use {
m.locationMutex.Lock()
m.cachedIPLat = nil
m.cachedIPLon = nil
m.locationMutex.Unlock()
}
m.TriggerUpdate()
}
func (m *Manager) TriggerUpdate() {
select {
case m.updateTrigger <- struct{}{}:
default:
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.wg.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) schedulerLoop() {
defer m.wg.Done()
var timer *time.Timer
for {
config := m.getConfig()
now := time.Now()
var isLight bool
var next time.Time
if config.Enabled {
isLight, next = m.computeSchedule(now, config)
} else {
m.stateMutex.RLock()
if m.state != nil {
isLight = m.state.IsLight
}
m.stateMutex.RUnlock()
next = now.Add(24 * time.Hour)
}
m.updateStateWithValues(config, isLight, next)
waitDur := time.Until(next)
if !config.Enabled {
waitDur = 24 * time.Hour
}
if waitDur < time.Second {
waitDur = time.Second
}
if timer != nil {
timer.Stop()
}
timer = time.NewTimer(waitDur)
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.updateTrigger:
timer.Stop()
continue
case <-timer.C:
continue
}
}
}
func (m *Manager) updateState(now time.Time) {
config := m.getConfig()
var isLight bool
var next time.Time
if config.Enabled {
isLight, next = m.computeSchedule(now, config)
} else {
m.stateMutex.RLock()
if m.state != nil {
isLight = m.state.IsLight
}
m.stateMutex.RUnlock()
next = now.Add(24 * time.Hour)
}
m.updateStateWithValues(config, isLight, next)
}
func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
newState := State{
Config: config,
IsLight: isLight,
NextTransition: next,
}
m.stateMutex.Lock()
if m.state != nil && statesEqual(m.state, &newState) {
m.stateMutex.Unlock()
return
}
m.state = &newState
m.stateMutex.Unlock()
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
state := m.GetState()
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- state:
default:
}
return true
})
}
func (m *Manager) getConfig() Config {
m.configMutex.RLock()
defer m.configMutex.RUnlock()
return m.config
}
func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude
}
if !config.UseIPLocation {
return nil, nil
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
lat, lon, err := wayland.FetchIPLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = lat
m.cachedIPLon = lon
m.locationMutex.Unlock()
return lat, lon
}
func statesEqual(a, b *State) bool {
if a == nil || b == nil {
return a == b
}
if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
return false
}
return a.Config == b.Config
}
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
if config.Mode == "location" {
return m.computeLocationSchedule(now, config)
}
return computeTimeSchedule(now, config)
}
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
startMinutes := config.StartHour*60 + config.StartMinute
endMinutes := config.EndHour*60 + config.EndMinute
currentMinutes := now.Hour()*60 + now.Minute()
startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
if startMinutes == endMinutes {
next := startTime
if !next.After(now) {
next = next.Add(24 * time.Hour)
}
return true, next
}
if startMinutes < endMinutes {
if currentMinutes < startMinutes {
return true, startTime
}
if currentMinutes >= endMinutes {
return true, startTime.Add(24 * time.Hour)
}
return false, endTime
}
if currentMinutes >= startMinutes {
return false, endTime.Add(24 * time.Hour)
}
if currentMinutes < endMinutes {
return false, endTime
}
return true, startTime
}
func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
lat, lon := m.getLocation(config)
if lat == nil || lon == nil {
currentIsLight := false
m.stateMutex.RLock()
if m.state != nil {
currentIsLight = m.state.IsLight
}
m.stateMutex.RUnlock()
return currentIsLight, now.Add(10 * time.Minute)
}
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
if cond != wayland.SunNormal {
if cond == wayland.SunMidnightSun {
return true, startOfNextDay(now)
}
return false, startOfNextDay(now)
}
if now.Before(times.Sunrise) {
return false, times.Sunrise
}
if now.Before(times.Sunset) {
return true, times.Sunset
}
nextDay := startOfNextDay(now)
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
if nextCond != wayland.SunNormal {
if nextCond == wayland.SunMidnightSun {
return true, startOfNextDay(nextDay)
}
return false, startOfNextDay(nextDay)
}
return false, nextTimes.Sunrise
}
func startOfNextDay(t time.Time) time.Time {
next := t.Add(24 * time.Hour)
return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
}
func validateHourMinute(hour, minute int) bool {
if hour < 0 || hour > 23 {
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
}
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
return errInvalidTime
}
return nil
}
var errInvalidTime = errors.New("invalid schedule time")

View File

@@ -0,0 +1,23 @@
package thememode
import "time"
type Config struct {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
StartHour int `json:"startHour"`
StartMinute int `json:"startMinute"`
EndHour int `json:"endHour"`
EndMinute int `json:"endMinute"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
UseIPLocation bool `json:"useIPLocation"`
ElevationTwilight float64 `json:"elevationTwilight"`
ElevationDaylight float64 `json:"elevationDaylight"`
}
type State struct {
Config Config `json:"config"`
IsLight bool `json:"isLight"`
NextTransition time.Time `json:"nextTransition"`
}

View File

@@ -626,6 +626,7 @@ func (m *Manager) schedulerLoop() {
m.schedule.calcDay = time.Time{}
m.scheduleMutex.Unlock()
m.recalcSchedule(time.Now())
m.updateStateFromSchedule()
m.configMutex.RLock()
enabled := m.config.Enabled
m.configMutex.RUnlock()

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766651565,
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
"lastModified": 1769018530,
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
"type": "github"
},
"original": {

View File

@@ -47,6 +47,7 @@
kirigami.unwrapped
sonnet
qtmultimedia
qtimageformats
];
in
{

View File

@@ -13,7 +13,7 @@ import "settings/SessionStore.js" as Store
Singleton {
id: root
readonly property int sessionConfigVersion: 2
readonly property int sessionConfigVersion: 3
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool _parseError: false
@@ -82,6 +82,15 @@ Singleton {
property bool nightModeUseIPLocation: false
property string nightModeLocationProvider: ""
property bool themeModeAutoEnabled: false
property string themeModeAutoMode: "time"
property int themeModeStartHour: 18
property int themeModeStartMinute: 0
property int themeModeEndHour: 6
property int themeModeEndMinute: 0
property bool themeModeShareGammaSettings: true
property string themeModeNextTransition: ""
property var pinnedApps: []
property var barPinnedApps: []
property int dockLauncherPosition: 0
@@ -109,6 +118,8 @@ Singleton {
property var appOverrides: ({})
property bool searchAppActions: true
property string vpnLastConnected: ""
Component.onCompleted: {
if (!isGreeterMode) {
loadSettings();
@@ -172,7 +183,7 @@ Singleton {
} catch (e) {
_parseError = true;
const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
}
}
@@ -186,14 +197,10 @@ Singleton {
_isReadOnly = !writable;
if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges();
if (!wasReadOnly)
console.info("SessionData: session.json is now read-only");
} else {
_loadedSessionSnapshot = getCurrentSessionJson();
_hasUnsavedChanges = false;
if (wasReadOnly)
console.info("SessionData: session.json is now writable");
if (_pendingMigration)
if (wasReadOnly && _pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
}
_pendingMigration = null;
@@ -255,7 +262,7 @@ Singleton {
} catch (e) {
_parseError = true;
const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
}
}
@@ -273,7 +280,6 @@ Singleton {
}
function migrateFromUndefinedToV1(settings) {
console.info("SessionData: Migrating configuration from undefined to version 1");
if (typeof SettingsData !== "undefined") {
if (settings.acMonitorTimeout !== undefined) {
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
@@ -448,7 +454,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -545,7 +551,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -583,7 +589,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -621,7 +627,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -659,7 +665,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -702,7 +708,6 @@ Singleton {
}
function setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled);
nightModeAutoEnabled = enabled;
saveSettings();
}
@@ -738,13 +743,11 @@ Singleton {
}
function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat);
latitude = lat;
saveSettings();
}
function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng);
longitude = lng;
saveSettings();
}
@@ -754,6 +757,41 @@ Singleton {
saveSettings();
}
function setThemeModeAutoEnabled(enabled) {
themeModeAutoEnabled = enabled;
saveSettings();
}
function setThemeModeAutoMode(mode) {
themeModeAutoMode = mode;
saveSettings();
}
function setThemeModeStartHour(hour) {
themeModeStartHour = hour;
saveSettings();
}
function setThemeModeStartMinute(minute) {
themeModeStartMinute = minute;
saveSettings();
}
function setThemeModeEndHour(hour) {
themeModeEndHour = hour;
saveSettings();
}
function setThemeModeEndMinute(minute) {
themeModeEndMinute = minute;
saveSettings();
}
function setThemeModeShareGammaSettings(share) {
themeModeShareGammaSettings = share;
saveSettings();
}
function setPinnedApps(apps) {
pinnedApps = apps;
saveSettings();
@@ -1003,6 +1041,11 @@ Singleton {
saveSettings();
}
function setVpnLastConnected(uuid) {
vpnLastConnected = uuid || "";
saveSettings();
}
function syncWallpaperForCurrentMode() {
if (!perModeWallpaper)
return;

View File

@@ -292,13 +292,13 @@ Singleton {
property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
property string _legacyVpnLastConnected: ""
readonly property string weatherLocation: SessionData.weatherLocation
readonly property string weatherCoordinates: SessionData.weatherCoordinates
property bool useAutoLocation: false
property bool weatherEnabled: true
property string networkPreference: "auto"
property string vpnLastConnected: ""
property string iconTheme: "System Default"
property var availableIconThemes: ["System Default"]
@@ -1078,6 +1078,11 @@ Singleton {
_legacyWeatherLocation = obj.weatherLocation;
if (obj?.weatherCoordinates !== undefined)
_legacyWeatherCoordinates = obj.weatherCoordinates;
if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
_legacyVpnLastConnected = obj.vpnLastConnected;
SessionData.vpnLastConnected = _legacyVpnLastConnected;
SessionData.saveSettings();
}
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasLoaded = true;
@@ -2311,6 +2316,11 @@ Singleton {
_legacyWeatherLocation = obj.weatherLocation;
if (obj.weatherCoordinates !== undefined)
_legacyWeatherCoordinates = obj.weatherCoordinates;
if (obj.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
_legacyVpnLastConnected = obj.vpnLastConnected;
SessionData.vpnLastConnected = _legacyVpnLastConnected;
SessionData.saveSettings();
}
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasLoaded = true;

View File

@@ -94,6 +94,9 @@ Singleton {
property var matugenColors: ({})
property var _pendingGenerateParams: null
property bool themeModeAutomationActive: false
property bool dmsServiceWasDisconnected: true
readonly property var dank16: {
const raw = matugenColors?.dank16;
if (!raw)
@@ -176,6 +179,237 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false);
}
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
startThemeModeAutomation();
}
}
Connections {
target: SessionData
enabled: typeof SessionData !== "undefined"
function onThemeModeAutoEnabledChanged() {
if (SessionData.themeModeAutoEnabled) {
root.startThemeModeAutomation();
} else {
root.stopThemeModeAutomation();
}
}
function onThemeModeAutoModeChanged() {
if (root.themeModeAutomationActive) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
}
}
function onThemeModeStartHourChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeStartMinuteChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeEndHourChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeEndMinuteChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeShareGammaSettingsChanged() {
if (root.themeModeAutomationActive) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
}
}
function onNightModeStartHourChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeStartMinuteChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeEndHourChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeEndMinuteChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onLatitudeChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (!SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 &&
SessionData.longitude !== 0.0 &&
typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
function onLongitudeChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (!SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 &&
SessionData.longitude !== 0.0 &&
typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
function onNightModeUseIPLocationChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": SessionData.nightModeUseIPLocation
}, response => {
if (!response.error && !SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
}
// React to gamma backend's isDay state changes for location-based mode
Connections {
target: DisplayService
enabled: typeof DisplayService !== "undefined" &&
typeof SessionData !== "undefined" &&
SessionData.themeModeAutoEnabled &&
SessionData.themeModeAutoMode === "location" &&
!themeAutoBackendAvailable()
function onGammaIsDayChanged() {
if (root.isLightMode !== DisplayService.gammaIsDay) {
root.setLightMode(DisplayService.gammaIsDay, true, true);
}
}
}
Connections {
target: DMSService
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
function onLoginctlEvent(event) {
if (!SessionData.themeModeAutoEnabled) return;
if (event.event === "unlock" || event.event === "resume") {
if (!themeAutoBackendAvailable()) {
root.evaluateThemeMode();
return;
}
DMSService.sendRequest("theme.auto.trigger", {});
}
}
function onThemeAutoStateUpdate(data) {
if (!SessionData.themeModeAutoEnabled) {
return;
}
applyThemeAutoState(data);
}
function onConnectionStateChanged() {
if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") {
root.syncTimeThemeSchedule();
}
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
root.syncLocationThemeSchedule();
}
if (themeAutoBackendAvailable() && SessionData.themeModeAutoEnabled) {
DMSService.sendRequest("theme.auto.getState", null, response => {
if (response && response.result) {
applyThemeAutoState(response.result);
}
});
}
if (!SessionData.themeModeAutoEnabled) {
return;
}
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": true
}, response => {
if (!response.error) {
console.info("Theme automation: IP location enabled after connection");
}
});
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (!response.error) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, locationResponse => {
if (locationResponse?.error) {
console.warn("Theme automation: Failed to set location", locationResponse.error);
}
});
}
});
} else {
console.warn("Theme automation: No location configured");
}
}
}
}
function applyGreeterTheme(themeName) {
@@ -491,7 +725,9 @@ Singleton {
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
function screenTransition() {
CompositorService.isNiri && NiriService.doScreenTransition();
if (CompositorService.isNiri) {
NiriService.doScreenTransition();
}
}
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
@@ -543,8 +779,10 @@ Singleton {
}
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
SessionData.setLightMode(light);
}
if (!isGreeterMode) {
// Skip with matugen because, our script runner will do it.
if (!matugenAvailable) {
@@ -552,6 +790,7 @@ Singleton {
}
generateSystemThemesFromCurrentTheme();
}
}
function toggleLightMode(savePrefs = true) {
@@ -1233,7 +1472,7 @@ Singleton {
return `#${invR}${invG}${invB}`;
}
property string baseLogoColor: {
property var baseLogoColor: {
if (typeof SettingsData === "undefined")
return "";
const colorOverride = SettingsData.launcherLogoColorOverride;
@@ -1246,7 +1485,7 @@ Singleton {
return colorOverride;
}
property string effectiveLogoColor: {
property var effectiveLogoColor: {
if (typeof SettingsData === "undefined")
return "";
@@ -1453,4 +1692,297 @@ Singleton {
root.switchTheme(defaultTheme, true, false);
}
}
// Theme mode automation functions
function themeAutoBackendAvailable() {
return typeof DMSService !== "undefined" &&
DMSService.isConnected &&
Array.isArray(DMSService.capabilities) &&
DMSService.capabilities.includes("theme.auto");
}
function applyThemeAutoState(state) {
if (!state) {
return;
}
if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) {
return;
}
if (typeof SessionData !== "undefined" && state.nextTransition !== undefined) {
SessionData.themeModeNextTransition = state.nextTransition || "";
}
if (state.isLight !== undefined && root.isLightMode !== state.isLight) {
root.setLightMode(state.isLight, true, true);
}
}
function syncTimeThemeSchedule() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return;
}
if (!DMSService.isConnected) {
return;
}
const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time";
if (!timeModeActive) {
return;
}
DMSService.sendRequest("theme.auto.setMode", {"mode": "time"});
const shareSettings = SessionData.themeModeShareGammaSettings;
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
DMSService.sendRequest("theme.auto.setSchedule", {
"startHour": startHour,
"startMinute": startMinute,
"endHour": endHour,
"endMinute": endMinute
}, response => {
if (response && response.error) {
console.error("Theme automation: Failed to sync time schedule:", response.error);
}
});
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": true});
DMSService.sendRequest("theme.auto.trigger", {});
}
function syncLocationThemeSchedule() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return;
}
if (!DMSService.isConnected) {
return;
}
const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location";
if (!locationModeActive) {
return;
}
DMSService.sendRequest("theme.auto.setMode", {"mode": "location"});
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("theme.auto.setUseIPLocation", {"use": true});
} else {
DMSService.sendRequest("theme.auto.setUseIPLocation", {"use": false});
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("theme.auto.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
}
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": true});
DMSService.sendRequest("theme.auto.trigger", {});
}
function evaluateThemeMode() {
if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) {
return;
}
if (themeAutoBackendAvailable()) {
DMSService.sendRequest("theme.auto.getState", null, response => {
if (response && response.result) {
applyThemeAutoState(response.result);
}
});
return;
}
const mode = SessionData.themeModeAutoMode;
if (mode === "location") {
evaluateLocationBasedThemeMode();
} else {
evaluateTimeBasedThemeMode();
}
}
function evaluateLocationBasedThemeMode() {
if (typeof DisplayService !== "undefined") {
const shouldBeLight = DisplayService.gammaIsDay;
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
return;
}
if (!SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 &&
SessionData.longitude !== 0.0) {
const shouldBeLight = calculateIsDaytime(
SessionData.latitude,
SessionData.longitude
);
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
return;
}
if (root.themeModeAutomationActive) {
if (SessionData.nightModeUseIPLocation) {
console.warn("Theme automation: Waiting for IP location from backend");
} else {
console.warn("Theme automation: Location mode requires coordinates");
}
}
}
function evaluateTimeBasedThemeMode() {
const shareSettings = SessionData.themeModeShareGammaSettings;
const startHour = shareSettings ?
SessionData.nightModeStartHour : SessionData.themeModeStartHour;
const startMinute = shareSettings ?
SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
const endHour = shareSettings ?
SessionData.nightModeEndHour : SessionData.themeModeEndHour;
const endMinute = shareSettings ?
SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const startMinutes = startHour * 60 + startMinute;
const endMinutes = endHour * 60 + endMinute;
let shouldBeLight;
if (startMinutes < endMinutes) {
shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes;
} else {
shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes;
}
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
}
function calculateIsDaytime(lat, lng) {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 0);
const diff = now - start;
const dayOfYear = Math.floor(diff / 86400000);
const latRad = lat * Math.PI / 180;
const declination = 23.45 * Math.sin((360/365) * (dayOfYear - 81) * Math.PI / 180);
const declinationRad = declination * Math.PI / 180;
const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad);
if (cosHourAngle > 1) {
return false; // Polar night
}
if (cosHourAngle < -1) {
return true; // Midnight sun
}
const hourAngle = Math.acos(cosHourAngle);
const hourAngleDeg = hourAngle * 180 / Math.PI;
const sunriseHour = 12 - hourAngleDeg / 15;
const sunsetHour = 12 + hourAngleDeg / 15;
const timeZoneOffset = now.getTimezoneOffset() / 60;
const localSunrise = sunriseHour - lng / 15 - timeZoneOffset;
const localSunset = sunsetHour - lng / 15 - timeZoneOffset;
const currentHour = now.getHours() + now.getMinutes() / 60;
const normalizeSunrise = ((localSunrise % 24) + 24) % 24;
const normalizeSunset = ((localSunset % 24) + 24) % 24;
return currentHour >= normalizeSunrise && currentHour < normalizeSunset;
}
// Helper function to send location to backend
function sendLocationToBackend() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return false;
}
if (!DMSService.isConnected) {
return false;
}
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {"use": true}, response => {
if (response?.error) {
console.warn("Theme automation: Failed to enable IP location", response.error);
}
});
return true;
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {"use": false}, response => {
if (!response.error) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, locResp => {
if (locResp?.error) {
console.warn("Theme automation: Failed to set location", locResp.error);
}
});
}
});
return true;
}
return false;
}
Timer {
id: locationRetryTimer
interval: 1000
repeat: true
running: false
property int retryCount: 0
onTriggered: {
if (root.sendLocationToBackend()) {
stop();
retryCount = 0;
root.evaluateThemeMode();
} else {
retryCount++;
if (retryCount >= 10) {
stop();
retryCount = 0;
}
}
}
}
function startThemeModeAutomation() {
root.themeModeAutomationActive = true;
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
const sent = root.sendLocationToBackend();
if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") {
locationRetryTimer.start();
} else {
root.evaluateThemeMode();
}
}
function stopThemeModeAutomation() {
root.themeModeAutomationActive = false;
if (typeof DMSService !== "undefined" && DMSService.isConnected) {
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": false});
}
}
}

View File

@@ -35,6 +35,14 @@ var SPEC = {
nightModeUseIPLocation: { def: false },
nightModeLocationProvider: { def: "" },
themeModeAutoEnabled: { def: false },
themeModeAutoMode: { def: "time" },
themeModeStartHour: { def: 18 },
themeModeStartMinute: { def: 0 },
themeModeEndHour: { def: 6 },
themeModeEndMinute: { def: 0 },
themeModeShareGammaSettings: { def: true },
weatherLocation: { def: "New York, NY" },
weatherCoordinates: { def: "40.7128,-74.0060" },
@@ -61,7 +69,9 @@ var SPEC = {
hiddenApps: { def: [] },
appOverrides: { def: {} },
searchAppActions: { def: true }
searchAppActions: { def: true },
vpnLastConnected: { def: "" }
};
function getValidKeys() {

View File

@@ -1,6 +1,6 @@
.pragma library
.import "./SessionSpec.js" as SpecModule
.import "./SessionSpec.js" as SpecModule
function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC;
@@ -68,6 +68,11 @@ function migrateToVersion(obj, targetVersion, settingsData) {
session.configVersion = 2;
}
if (currentVersion < 3) {
console.info("SessionData: Migrating session to version 3");
session.configVersion = 3;
}
return session;
}

View File

@@ -79,16 +79,18 @@ var SPEC = {
privacyShowCameraIcon: { def: false },
privacyShowScreenShareIcon: { def: false },
controlCenterWidgets: { def: [
{ id: "volumeSlider", enabled: true, width: 50 },
{ id: "brightnessSlider", enabled: true, width: 50 },
{ id: "wifi", enabled: true, width: 50 },
{ id: "bluetooth", enabled: true, width: 50 },
{ id: "audioOutput", enabled: true, width: 50 },
{ id: "audioInput", enabled: true, width: 50 },
{ id: "nightMode", enabled: true, width: 50 },
{ id: "darkMode", enabled: true, width: 50 }
]},
controlCenterWidgets: {
def: [
{ id: "volumeSlider", enabled: true, width: 50 },
{ id: "brightnessSlider", enabled: true, width: 50 },
{ id: "wifi", enabled: true, width: 50 },
{ id: "bluetooth", enabled: true, width: 50 },
{ id: "audioOutput", enabled: true, width: 50 },
{ id: "audioInput", enabled: true, width: 50 },
{ id: "nightMode", enabled: true, width: 50 },
{ id: "darkMode", enabled: true, width: 50 }
]
},
showWorkspaceIndex: { def: false },
showWorkspaceName: { def: false },
@@ -119,13 +121,15 @@ var SPEC = {
keyboardLayoutNameCompactMode: { def: false },
runningAppsCurrentWorkspace: { def: false },
runningAppsGroupByApp: { def: false },
appIdSubstitutions: { def: [
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
]},
appIdSubstitutions: {
def: [
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
]
},
centeringMode: { def: "index" },
clockDateFormat: { def: "" },
lockDateFormat: { def: "" },
@@ -153,7 +157,6 @@ var SPEC = {
weatherEnabled: { def: true },
networkPreference: { def: "auto" },
vpnLastConnected: { def: "" },
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
availableIconThemes: { def: ["System Default"], persist: false },
@@ -306,7 +309,7 @@ var SPEC = {
osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },
osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled : { def: true },
osdMediaVolumeEnabled: { def: true },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
@@ -337,52 +340,54 @@ var SPEC = {
niriOutputSettings: { def: {} },
hyprlandOutputSettings: { def: {} },
barConfigs: { def: [{
id: "default",
name: "Main Bar",
enabled: true,
position: 0,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4,
innerPadding: 4,
bottomGap: 0,
transparency: 1.0,
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
borderEnabled: false,
borderColor: "surfaceText",
borderOpacity: 1.0,
borderThickness: 1,
widgetOutlineEnabled: false,
widgetOutlineColor: "primary",
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
autoHide: false,
autoHideDelay: 250,
showOnWindowsOpen: false,
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace",
shadowIntensity: 0,
shadowOpacity: 60,
shadowColorMode: "text",
shadowCustomColor: "#000000",
clickThrough: false
}], onChange: "updateBarConfigs" },
barConfigs: {
def: [{
id: "default",
name: "Main Bar",
enabled: true,
position: 0,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4,
innerPadding: 4,
bottomGap: 0,
transparency: 1.0,
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
borderEnabled: false,
borderColor: "surfaceText",
borderOpacity: 1.0,
borderThickness: 1,
widgetOutlineEnabled: false,
widgetOutlineColor: "primary",
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
autoHide: false,
autoHideDelay: 250,
showOnWindowsOpen: false,
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace",
shadowIntensity: 0,
shadowOpacity: 60,
shadowColorMode: "text",
shadowCustomColor: "#000000",
clickThrough: false
}], onChange: "updateBarConfigs"
},
desktopClockEnabled: { def: false },
desktopClockStyle: { def: "analog" },
@@ -437,7 +442,7 @@ var SPEC = {
};
function getValidKeys() {
return Object.keys(SPEC).filter(function(k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
return Object.keys(SPEC).filter(function (k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
}
function set(root, key, value, saveFn, hooks) {

View File

@@ -1,6 +1,6 @@
.pragma library
.import "./SettingsSpec.js" as SpecModule
.import "./SettingsSpec.js" as SpecModule
function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC;

View File

@@ -1114,6 +1114,79 @@ Item {
target: "spotlight"
}
IpcHandler {
function info(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showInfo(message);
return "TOAST_INFO_SUCCESS";
}
function infoWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showInfo(message, details, command, category);
return "TOAST_INFO_SUCCESS";
}
function warn(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showWarning(message);
return "TOAST_WARN_SUCCESS";
}
function warnWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showWarning(message, details, command, category);
return "TOAST_WARN_SUCCESS";
}
function error(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showError(message);
return "TOAST_ERROR_SUCCESS";
}
function errorWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showError(message, details, command, category);
return "TOAST_ERROR_SUCCESS";
}
function hide(): string {
ToastService.hideToast();
return "TOAST_HIDE_SUCCESS";
}
function dismiss(category: string): string {
if (!category)
return "ERROR: No category specified";
ToastService.dismissCategory(category);
return "TOAST_DISMISS_SUCCESS";
}
function status(): string {
if (!ToastService.toastVisible)
return "hidden";
const levels = ["info", "warn", "error"];
return `visible:${levels[ToastService.currentLevel]}:${ToastService.currentMessage}`;
}
target: "toast"
}
IpcHandler {
function open(): string {
FirstLaunchService.showWelcome();

View File

@@ -33,7 +33,8 @@ Rectangle {
result.push(selectedItem.primaryAction);
}
if (selectedItem?.type === "plugin") {
switch (selectedItem?.type) {
case "plugin":
var pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i];
@@ -44,7 +45,17 @@ Rectangle {
pluginAction: act.action
});
}
} else if (selectedItem?.type === "app" && !selectedItem?.isCore) {
break;
case "plugin_browse":
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
break;
case "app":
if (selectedItem?.isCore)
break;
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
@@ -57,18 +68,22 @@ Rectangle {
action: "launch_dgpu"
});
}
break;
}
return result;
}
readonly property bool hasActions: {
if (selectedItem?.type === "app" && !selectedItem?.isCore)
return true;
if (selectedItem?.type === "plugin") {
var pluginActions = getPluginContextMenuActions();
return pluginActions.length > 0;
switch (selectedItem?.type) {
case "app":
return !selectedItem?.isCore;
case "plugin":
return getPluginContextMenuActions().length > 0;
case "plugin_browse":
return selectedItem?.actions?.length > 0;
default:
return actions.length > 1;
}
return actions.length > 1;
}
width: parent?.width ?? 200

View File

@@ -6,6 +6,9 @@ import Quickshell.Io
import qs.Common
import qs.Services
import "Scorer.js" as Scorer
import "ControllerUtils.js" as Utils
import "NavigationHelpers.js" as Nav
import "ItemTransformers.js" as Transform
Item {
id: root
@@ -45,9 +48,14 @@ Item {
Connections {
target: PluginService
function onRequestLauncherUpdate(pluginId) {
if (activePluginId === pluginId || searchQuery) {
if (activePluginId === pluginId) {
if (activePluginCategories.length <= 1)
loadPluginCategories(pluginId);
performSearch();
return;
}
if (searchQuery)
performSearch();
}
}
@@ -130,6 +138,8 @@ Item {
property string pluginFilter: ""
property string activePluginName: ""
property var activePluginCategories: []
property string activePluginCategory: ""
function getSectionViewMode(sectionId) {
if (sectionId === "browse_plugins")
@@ -147,6 +157,10 @@ Item {
if (sectionDefinitions[i].id === sectionId)
return sectionDefinitions[i].defaultViewMode || "list";
}
if (pluginViewPreferences[sectionId]?.mode)
return pluginViewPreferences[sectionId].mode;
return "list";
}
@@ -300,10 +314,33 @@ Item {
isSearching = false;
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
pluginFilter = "";
collapsedSections = {};
}
function loadPluginCategories(pluginId) {
if (!pluginId) {
activePluginCategories = [];
activePluginCategory = "";
return;
}
const categories = AppSearchService.getPluginLauncherCategories(pluginId);
activePluginCategories = categories;
activePluginCategory = "";
AppSearchService.setPluginLauncherCategory(pluginId, "");
}
function setActivePluginCategory(categoryId) {
if (activePluginCategory === categoryId)
return;
activePluginCategory = categoryId;
AppSearchService.setPluginLauncherCategory(activePluginId, categoryId);
performSearch();
}
function clearPluginFilter() {
if (pluginFilter) {
pluginFilter = "";
@@ -313,14 +350,30 @@ Item {
return false;
}
function preserveSelectionAfterUpdate() {
var previousSelectedId = selectedItem?.id || "";
return function (newFlatModel) {
if (!previousSelectedId)
return getFirstItemIndex();
for (var i = 0; i < newFlatModel.length; i++) {
if (!newFlatModel[i].isHeader && newFlatModel[i].item?.id === previousSelectedId)
return i;
}
return getFirstItemIndex();
};
}
function performSearch() {
var currentVersion = _searchVersion;
isSearching = true;
var restoreSelection = preserveSelectionAfterUpdate();
var cachedSections = AppSearchService.getCachedDefaultSections();
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference();
sections = cachedSections.map(function (s) {
var copy = Object.assign({}, s, {
@@ -331,7 +384,7 @@ Item {
return copy;
});
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
searchCompleted();
@@ -342,10 +395,14 @@ Item {
var triggerMatch = detectTrigger(searchQuery);
if (triggerMatch.pluginId) {
var pluginChanged = activePluginId !== triggerMatch.pluginId;
activePluginId = triggerMatch.pluginId;
activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn);
applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn);
if (pluginChanged && !triggerMatch.isBuiltIn)
loadPluginCategories(triggerMatch.pluginId);
var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
allItems = allItems.concat(pluginItems);
@@ -370,7 +427,7 @@ Item {
}
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -380,6 +437,8 @@ Item {
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference();
if (searchMode === "files") {
@@ -409,7 +468,7 @@ Item {
return copy;
});
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
searchCompleted();
@@ -434,7 +493,7 @@ Item {
}
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -489,7 +548,7 @@ Item {
}
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -573,7 +632,7 @@ Item {
AppSearchService.setCachedDefaultSections(sections, flatModel);
}
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -674,241 +733,26 @@ Item {
function transformApp(app) {
var appId = app.id || app.execString || app.exec || "";
var override = SessionData.getAppOverride(appId);
var actions = [];
if (app.actions && app.actions.length > 0) {
for (var i = 0; i < app.actions.length; i++) {
actions.push({
name: app.actions[i].name,
icon: "play_arrow",
actionData: app.actions[i]
});
}
}
return {
id: appId,
type: "app",
name: override?.name || app.name || "",
subtitle: override?.comment || app.comment || "",
icon: override?.icon || app.icon || "application-x-executable",
iconType: "image",
section: "apps",
data: app,
keywords: app.keywords || [],
actions: actions,
primaryAction: {
name: I18n.tr("Launch"),
icon: "open_in_new",
action: "launch"
}
};
return Transform.transformApp(app, override, [], I18n.tr("Launch"));
}
function transformCoreApp(app) {
var iconName = "apps";
var iconType = "material";
if (app.icon) {
if (app.icon.startsWith("svg+corner:")) {
iconType = "composite";
} else if (app.icon.startsWith("material:")) {
iconName = app.icon.substring(9);
} else {
iconName = app.icon;
iconType = "image";
}
}
return {
id: app.builtInPluginId || app.action || "",
type: "app",
name: app.name || "",
subtitle: app.comment || "",
icon: iconName,
iconType: iconType,
iconFull: app.icon,
section: "apps",
data: app,
isCore: true,
actions: [],
primaryAction: {
name: I18n.tr("Open"),
icon: "open_in_new",
action: "launch"
}
};
return Transform.transformCoreApp(app, I18n.tr("Open"));
}
function transformBuiltInLauncherItem(item, pluginId) {
var rawIcon = item.icon || "extension";
var icon = stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.action || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
isBuiltInLauncher: true,
keywords: item.keywords || [],
actions: [],
primaryAction: {
name: I18n.tr("Open"),
icon: "open_in_new",
action: "execute"
}
};
return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open"));
}
function transformFileResult(file) {
var filename = file.path ? file.path.split("/").pop() : "";
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
return {
id: file.path || "",
type: "file",
name: filename,
subtitle: dirname,
icon: getFileIcon(filename),
iconType: "material",
section: "files",
data: file,
actions: [
{
name: I18n.tr("Open folder"),
icon: "folder_open",
action: "open_folder"
},
{
name: I18n.tr("Copy path"),
icon: "content_copy",
action: "copy_path"
}
],
primaryAction: {
name: I18n.tr("Open"),
icon: "open_in_new",
action: "open"
}
};
}
function getFileIcon(filename) {
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
var iconMap = {
"pdf": "picture_as_pdf",
"doc": "description",
"docx": "description",
"odt": "description",
"xls": "table_chart",
"xlsx": "table_chart",
"ods": "table_chart",
"ppt": "slideshow",
"pptx": "slideshow",
"odp": "slideshow",
"txt": "article",
"md": "article",
"rst": "article",
"jpg": "image",
"jpeg": "image",
"png": "image",
"gif": "image",
"svg": "image",
"webp": "image",
"mp3": "audio_file",
"wav": "audio_file",
"flac": "audio_file",
"ogg": "audio_file",
"mp4": "video_file",
"mkv": "video_file",
"avi": "video_file",
"webm": "video_file",
"zip": "folder_zip",
"tar": "folder_zip",
"gz": "folder_zip",
"7z": "folder_zip",
"rar": "folder_zip",
"js": "code",
"ts": "code",
"py": "code",
"rs": "code",
"go": "code",
"java": "code",
"c": "code",
"cpp": "code",
"h": "code",
"html": "web",
"css": "web",
"htm": "web",
"json": "data_object",
"xml": "data_object",
"yaml": "data_object",
"yml": "data_object",
"sh": "terminal",
"bash": "terminal",
"zsh": "terminal"
};
return iconMap[ext] || "insert_drive_file";
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"));
}
function evaluateCalculator(query) {
if (!query || query.length === 0)
var calc = Utils.evaluateCalculator(query);
if (!calc)
return null;
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
if (mathExpr.length < 2)
return null;
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
if (!hasMath)
return null;
try {
var sanitized = mathExpr.replace(/\^/g, "**");
var result = Function('"use strict"; return (' + sanitized + ')')();
if (typeof result === "number" && isFinite(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
return {
id: "calculator_result",
type: "calculator",
name: displayResult,
subtitle: query + " =",
icon: "calculate",
iconType: "material",
section: "calculator",
data: {
expression: query,
result: result
},
actions: [],
primaryAction: {
name: I18n.tr("Copy"),
icon: "content_copy",
action: "copy"
}
};
}
} catch (e) {}
return null;
return Transform.createCalculatorItem(calc, query, I18n.tr("Copy"));
}
function detectTrigger(query) {
@@ -980,17 +824,7 @@ Item {
}
function sortPluginIdsByOrder(pluginIds) {
var order = SettingsData.launcherPluginOrder || [];
if (order.length === 0)
return pluginIds;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return pluginIds.slice().sort(function (a, b) {
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
return aOrder - bOrder;
});
return Utils.sortPluginIdsByOrder(pluginIds, SettingsData.launcherPluginOrder || []);
}
function getAllVisiblePluginsOrdered() {
@@ -1011,17 +845,7 @@ Item {
isBuiltIn: true
});
}
var order = SettingsData.launcherPluginOrder || [];
if (order.length === 0)
return all;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return all.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
});
return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
}
function getEmptyTriggerPluginsOrdered() {
@@ -1044,72 +868,27 @@ Item {
isBuiltIn: true
});
}
var order = SettingsData.launcherPluginOrder || [];
if (order.length === 0)
return all;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return all.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
});
return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
}
function getPluginBrowseItems() {
var items = [];
var browseLabel = I18n.tr("Browse");
var triggerLabel = I18n.tr("Trigger: %1");
var noTriggerLabel = I18n.tr("No trigger");
var launchers = PluginService.getLauncherPlugins();
for (var pluginId in launchers) {
var plugin = launchers[pluginId];
var trigger = PluginService.getPluginTrigger(pluginId);
var rawIcon = plugin.icon || "extension";
items.push({
id: "browse_" + pluginId,
type: "plugin_browse",
name: plugin.name || pluginId,
subtitle: trigger ? I18n.tr("Trigger: %1").arg(trigger) : I18n.tr("No trigger"),
icon: stripIconPrefix(rawIcon),
iconType: detectIconType(rawIcon),
section: "browse_plugins",
data: {
pluginId: pluginId,
plugin: plugin
},
actions: [],
primaryAction: {
name: I18n.tr("Browse"),
icon: "arrow_forward",
action: "browse_plugin"
}
});
var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
items.push(Transform.createPluginBrowseItem(pluginId, launchers[pluginId], trigger, false, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
}
var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins();
for (var pluginId in builtInLaunchers) {
var plugin = builtInLaunchers[pluginId];
var trigger = AppSearchService.getBuiltInPluginTrigger(pluginId);
items.push({
id: "browse_" + pluginId,
type: "plugin_browse",
name: plugin.name || pluginId,
subtitle: trigger ? I18n.tr("Trigger: %1").arg(trigger) : I18n.tr("No trigger"),
icon: plugin.cornerIcon || "extension",
iconType: "material",
section: "browse_plugins",
data: {
pluginId: pluginId,
plugin: plugin,
isBuiltIn: true
},
actions: [],
primaryAction: {
name: I18n.tr("Browse"),
icon: "arrow_forward",
action: "browse_plugin"
}
});
var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
items.push(Transform.createPluginBrowseItem(pluginId, builtInLaunchers[pluginId], trigger, true, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
}
return items;
@@ -1134,34 +913,6 @@ Item {
return transformed;
}
function detectIconType(iconName) {
if (!iconName)
return "material";
if (iconName.startsWith("unicode:"))
return "unicode";
if (iconName.startsWith("material:"))
return "material";
if (iconName.startsWith("image:"))
return "image";
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
return "image";
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
return "image";
return "material";
}
function stripIconPrefix(iconName) {
if (!iconName)
return "extension";
if (iconName.startsWith("unicode:"))
return iconName.substring(8);
if (iconName.startsWith("material:"))
return iconName.substring(9);
if (iconName.startsWith("image:"))
return iconName.substring(6);
return iconName;
}
function getPluginName(pluginId, isBuiltIn) {
if (isBuiltIn) {
var plugin = AppSearchService.builtInPlugins[pluginId];
@@ -1187,7 +938,7 @@ Item {
var rawIcon = launchers[pluginId].icon || "extension";
return {
name: launchers[pluginId].name || pluginId,
icon: stripIconPrefix(rawIcon)
icon: Utils.stripIconPrefix(rawIcon)
};
}
return {
@@ -1219,9 +970,8 @@ Item {
defaultViewMode: viewPref.mode || "list"
};
if (viewPref.enforced) {
setPluginViewPreference(section, viewPref.mode, true);
}
if (viewPref.mode)
setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
basePriority += 0.01;
}
@@ -1257,36 +1007,7 @@ Item {
}
function transformPluginItem(item, pluginId) {
var rawIcon = item.icon || "extension";
var icon = stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.id || item.name || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || item.description || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
keywords: item.keywords || [],
actions: item.actions || [],
primaryAction: item.primaryAction || {
name: I18n.tr("Select"),
icon: "check",
action: "execute"
}
};
return Transform.transformPluginItem(item, pluginId, I18n.tr("Select"));
}
function getFrecencyForItem(item) {
@@ -1312,11 +1033,7 @@ Item {
}
function getFirstItemIndex() {
for (var i = 0; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return 0;
return Nav.getFirstItemIndex(flatModel);
}
function updateSelectedItem() {
@@ -1337,266 +1054,67 @@ Item {
return getSectionViewMode(entry.sectionId);
}
function findNextNonHeaderIndex(startIndex) {
for (var i = startIndex; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function findPrevNonHeaderIndex(startIndex) {
for (var i = startIndex; i >= 0; i--) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function getSectionBounds(sectionId) {
var start = -1, end = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
start = i + 1;
} else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
end = i;
} else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
break;
}
}
return {
start: start,
end: end,
count: end >= start ? end - start + 1 : 0
};
}
function getGridColumns(sectionId) {
var mode = getSectionViewMode(sectionId);
if (mode === "tile")
return 3;
if (mode === "grid")
return gridColumns;
return 1;
return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns);
}
function selectNext() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var cols = getGridColumns(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection + cols;
if (newPosInSection < bounds.count) {
selectedFlatIndex = bounds.start + newPosInSection;
var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
} else {
var nextSection = findNextNonHeaderIndex(bounds.end + 1);
if (nextSection !== -1) {
selectedFlatIndex = nextSection;
updateSelectedItem();
}
}
}
function selectPrevious() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var cols = getGridColumns(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection - cols;
if (newPosInSection >= 0) {
selectedFlatIndex = bounds.start + newPosInSection;
var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
} else {
var prevItem = findPrevNonHeaderIndex(bounds.start - 1);
if (prevItem !== -1) {
selectedFlatIndex = prevItem;
updateSelectedItem();
}
}
}
function selectRight() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection + 1 < bounds.count) {
selectedFlatIndex = bounds.start + posInSection + 1;
var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectLeft() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection > 0) {
selectedFlatIndex = bounds.start + posInSection - 1;
var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectNextSection() {
keyboardNavigationActive = true;
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var foundCurrent = false;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (foundCurrent) {
for (var j = i + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader) {
selectedFlatIndex = j;
updateSelectedItem();
return;
}
}
}
if (flatModel[i].section.id === currentSection) {
foundCurrent = true;
}
}
var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectPreviousSection() {
keyboardNavigationActive = true;
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var lastSectionStart = -1;
var prevSectionStart = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (flatModel[i].section.id === currentSection) {
break;
}
prevSectionStart = lastSectionStart;
lastSectionStart = i;
}
}
if (prevSectionStart >= 0) {
for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader) {
selectedFlatIndex = j;
updateSelectedItem();
return;
}
}
var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectPageDown(visibleItems) {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var next = findNextNonHeaderIndex(newIndex + 1);
if (next === -1)
break;
newIndex = next;
}
var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1605,18 +1123,7 @@ Item {
function selectPageUp(visibleItems) {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var prev = findPrevNonHeaderIndex(newIndex - 1);
if (prev === -1)
break;
newIndex = prev;
}
var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1747,6 +1254,14 @@ Item {
launchAppWithNvidia(item.data);
}
break;
case "toggle_all_visibility":
if (item.type === "plugin_browse" && item.data?.pluginId) {
var pluginId = item.data.pluginId;
var currentState = SettingsData.getPluginAllowWithoutTrigger(pluginId);
SettingsData.setPluginAllowWithoutTrigger(pluginId, !currentState);
performSearch();
}
return;
default:
if (item.type === "app" && action.actionData) {
launchAppAction({

View File

@@ -0,0 +1,157 @@
.pragma library
function getFileIcon(filename) {
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
switch (ext) {
case "pdf":
return "picture_as_pdf";
case "doc":
case "docx":
case "odt":
return "description";
case "xls":
case "xlsx":
case "ods":
return "table_chart";
case "ppt":
case "pptx":
case "odp":
return "slideshow";
case "txt":
case "md":
case "rst":
return "article";
case "jpg":
case "jpeg":
case "png":
case "gif":
case "svg":
case "webp":
return "image";
case "mp3":
case "wav":
case "flac":
case "ogg":
return "audio_file";
case "mp4":
case "mkv":
case "avi":
case "webm":
return "video_file";
case "zip":
case "tar":
case "gz":
case "7z":
case "rar":
return "folder_zip";
case "js":
case "ts":
case "py":
case "rs":
case "go":
case "java":
case "c":
case "cpp":
case "h":
return "code";
case "html":
case "css":
case "htm":
return "web";
case "json":
case "xml":
case "yaml":
case "yml":
return "data_object";
case "sh":
case "bash":
case "zsh":
return "terminal";
default:
return "insert_drive_file";
}
}
function stripIconPrefix(iconName) {
if (!iconName)
return "extension";
if (iconName.startsWith("unicode:"))
return iconName.substring(8);
if (iconName.startsWith("material:"))
return iconName.substring(9);
if (iconName.startsWith("image:"))
return iconName.substring(6);
return iconName;
}
function detectIconType(iconName) {
if (!iconName)
return "material";
if (iconName.startsWith("unicode:"))
return "unicode";
if (iconName.startsWith("material:"))
return "material";
if (iconName.startsWith("image:"))
return "image";
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
return "image";
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
return "image";
return "material";
}
function evaluateCalculator(query) {
if (!query || query.length === 0)
return null;
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
if (mathExpr.length < 2)
return null;
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
if (!hasMath)
return null;
try {
var sanitized = mathExpr.replace(/\^/g, "**");
var result = Function('"use strict"; return (' + sanitized + ')')();
if (typeof result === "number" && isFinite(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
return {
expression: query,
result: result,
displayResult: displayResult
};
}
} catch (e) { }
return null;
}
function sortPluginIdsByOrder(pluginIds, order) {
if (!order || order.length === 0)
return pluginIds;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return pluginIds.slice().sort(function (a, b) {
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
return aOrder - bOrder;
});
}
function sortPluginsOrdered(plugins, order) {
if (!order || order.length === 0)
return plugins;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return plugins.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
});
}

View File

@@ -0,0 +1,223 @@
.pragma library
.import "ControllerUtils.js" as Utils
function transformApp(app, override, defaultActions, primaryActionLabel) {
var appId = app.id || app.execString || app.exec || "";
var actions = [];
if (app.actions && app.actions.length > 0) {
for (var i = 0; i < app.actions.length; i++) {
actions.push({
name: app.actions[i].name,
icon: "play_arrow",
actionData: app.actions[i]
});
}
}
return {
id: appId,
type: "app",
name: override?.name || app.name || "",
subtitle: override?.comment || app.comment || "",
icon: override?.icon || app.icon || "application-x-executable",
iconType: "image",
section: "apps",
data: app,
keywords: app.keywords || [],
actions: actions,
primaryAction: {
name: primaryActionLabel,
icon: "open_in_new",
action: "launch"
}
};
}
function transformCoreApp(app, openLabel) {
var iconName = "apps";
var iconType = "material";
if (app.icon) {
if (app.icon.startsWith("svg+corner:")) {
iconType = "composite";
} else if (app.icon.startsWith("material:")) {
iconName = app.icon.substring(9);
} else {
iconName = app.icon;
iconType = "image";
}
}
return {
id: app.builtInPluginId || app.action || "",
type: "app",
name: app.name || "",
subtitle: app.comment || "",
icon: iconName,
iconType: iconType,
iconFull: app.icon,
section: "apps",
data: app,
isCore: true,
actions: [],
primaryAction: {
name: openLabel,
icon: "open_in_new",
action: "launch"
}
};
}
function transformBuiltInLauncherItem(item, pluginId, openLabel) {
var rawIcon = item.icon || "extension";
var icon = Utils.stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.action || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
isBuiltInLauncher: true,
keywords: item.keywords || [],
actions: [],
primaryAction: {
name: openLabel,
icon: "open_in_new",
action: "execute"
}
};
}
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
var filename = file.path ? file.path.split("/").pop() : "";
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
return {
id: file.path || "",
type: "file",
name: filename,
subtitle: dirname,
icon: Utils.getFileIcon(filename),
iconType: "material",
section: "files",
data: file,
actions: [
{
name: openFolderLabel,
icon: "folder_open",
action: "open_folder"
},
{
name: copyPathLabel,
icon: "content_copy",
action: "copy_path"
}
],
primaryAction: {
name: openLabel,
icon: "open_in_new",
action: "open"
}
};
}
function transformPluginItem(item, pluginId, selectLabel) {
var rawIcon = item.icon || "extension";
var icon = Utils.stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.id || item.name || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || item.description || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
keywords: item.keywords || [],
actions: item.actions || [],
primaryAction: item.primaryAction || {
name: selectLabel,
icon: "check",
action: "execute"
}
};
}
function createCalculatorItem(calc, query, copyLabel) {
return {
id: "calculator_result",
type: "calculator",
name: calc.displayResult,
subtitle: query + " =",
icon: "calculate",
iconType: "material",
section: "calculator",
data: {
expression: calc.expression,
result: calc.result
},
actions: [],
primaryAction: {
name: copyLabel,
icon: "content_copy",
action: "copy"
}
};
}
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
return {
id: "browse_" + pluginId,
type: "plugin_browse",
name: plugin.name || pluginId,
subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
section: "browse_plugins",
data: {
pluginId: pluginId,
plugin: plugin,
isBuiltIn: isBuiltIn
},
actions: [
{
name: "All",
icon: isAllowed ? "visibility" : "visibility_off",
action: "toggle_all_visibility"
}
],
primaryAction: {
name: browseLabel,
icon: "arrow_forward",
action: "browse_plugin"
}
};
}

View File

@@ -483,9 +483,64 @@ FocusScope {
}
}
Row {
id: categoryRow
width: parent.width
height: controller.activePluginCategories.length > 0 ? 36 : 0
visible: controller.activePluginCategories.length > 0
spacing: Theme.spacingS
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
DankDropdown {
id: categoryDropdown
width: Math.min(200, parent.width)
compactMode: true
dropdownWidth: 200
popupWidth: 240
maxPopupHeight: 300
enableFuzzySearch: controller.activePluginCategories.length > 8
currentValue: {
const cats = controller.activePluginCategories;
const current = controller.activePluginCategory;
if (!current)
return cats.length > 0 ? cats[0].name : "";
for (let i = 0; i < cats.length; i++) {
if (cats[i].id === current)
return cats[i].name;
}
return cats.length > 0 ? cats[0].name : "";
}
options: {
const cats = controller.activePluginCategories;
const names = [];
for (let i = 0; i < cats.length; i++)
names.push(cats[i].name);
return names;
}
onValueChanged: value => {
const cats = controller.activePluginCategories;
for (let i = 0; i < cats.length; i++) {
if (cats[i].name === value) {
controller.setActivePluginCategory(cats[i].id);
return;
}
}
}
}
}
Item {
width: parent.width
height: parent.height - searchField.height - actionPanel.height - Theme.spacingXS * 2
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList {

View File

@@ -0,0 +1,245 @@
.pragma library
function getFirstItemIndex(flatModel) {
for (var i = 0; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return 0;
}
function findNextNonHeaderIndex(flatModel, startIndex) {
for (var i = startIndex; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function findPrevNonHeaderIndex(flatModel, startIndex) {
for (var i = startIndex; i >= 0; i--) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function getSectionBounds(flatModel, sectionId) {
var start = -1, end = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
start = i + 1;
} else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
end = i;
} else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
break;
}
}
return {
start: start,
end: end,
count: end >= start ? end - start + 1 : 0
};
}
function getGridColumns(viewMode, gridColumns) {
switch (viewMode) {
case "tile":
return 3;
case "grid":
return gridColumns;
default:
return 1;
}
}
function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
if (actualViewMode === "list") {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var cols = getGridColumns(actualViewMode, gridColumns);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection + cols;
if (newPosInSection < bounds.count) {
return bounds.start + newPosInSection;
}
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
return nextSection !== -1 ? nextSection : selectedFlatIndex;
}
function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
if (actualViewMode === "list") {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var cols = getGridColumns(actualViewMode, gridColumns);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection - cols;
if (newPosInSection >= 0) {
return bounds.start + newPosInSection;
}
var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1);
return prevItem !== -1 ? prevItem : selectedFlatIndex;
}
function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var viewMode = getSectionViewModeFn(entry.sectionId);
if (viewMode === "list") {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection + 1 < bounds.count) {
return bounds.start + posInSection + 1;
}
return selectedFlatIndex;
}
function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var viewMode = getSectionViewModeFn(entry.sectionId);
if (viewMode === "list") {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection > 0) {
return bounds.start + posInSection - 1;
}
return selectedFlatIndex;
}
function calculateNextSectionIndex(flatModel, selectedFlatIndex) {
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var foundCurrent = false;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (foundCurrent) {
for (var j = i + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader)
return j;
}
}
if (flatModel[i].section.id === currentSection) {
foundCurrent = true;
}
}
}
return selectedFlatIndex;
}
function calculatePrevSectionIndex(flatModel, selectedFlatIndex) {
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var lastSectionStart = -1;
var prevSectionStart = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (flatModel[i].section.id === currentSection) {
break;
}
prevSectionStart = lastSectionStart;
lastSectionStart = i;
}
}
if (prevSectionStart >= 0) {
for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader)
return j;
}
}
return selectedFlatIndex;
}
function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) {
if (flatModel.length === 0)
return selectedFlatIndex;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var next = findNextNonHeaderIndex(flatModel, newIndex + 1);
if (next === -1)
break;
newIndex = next;
}
return newIndex;
}
function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) {
if (flatModel.length === 0)
return selectedFlatIndex;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1);
if (prev === -1)
break;
newIndex = prev;
}
return newIndex;
}

View File

@@ -9,7 +9,7 @@ Rectangle {
property var item: null
property bool isSelected: false
property bool isHovered: itemArea.containsMouse
property bool isHovered: itemArea.containsMouse || allModeToggleArea.containsMouse
property var controller: null
property int flatIndex: -1
@@ -38,6 +38,29 @@ Rectangle {
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
radius: Theme.cornerRadius
MouseArea {
id: itemArea
anchors.fill: parent
anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
} else {
root.clicked();
}
}
onPositionChanged: {
if (root.controller)
root.controller.keyboardNavigationActive = false;
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
@@ -86,7 +109,47 @@ Rectangle {
spacing: Theme.spacingS
Rectangle {
visible: root.item?.type && root.item.type !== "app"
id: allModeToggle
visible: root.item?.type === "plugin_browse"
width: 28
height: 28
radius: 14
anchors.verticalCenter: parent.verticalCenter
color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
property bool isAllowed: {
if (root.item?.type !== "plugin_browse")
return false;
var pluginId = root.item?.data?.pluginId;
if (!pluginId)
return false;
SettingsData.launcherPluginVisibility;
return SettingsData.getPluginAllowWithoutTrigger(pluginId);
}
DankIcon {
anchors.centerIn: parent
name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
size: 18
color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: allModeToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var pluginId = root.item?.data?.pluginId;
if (!pluginId)
return;
SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
}
}
}
Rectangle {
visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
width: typeBadge.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10
@@ -116,27 +179,4 @@ Rectangle {
}
}
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
} else {
root.clicked();
}
}
onPositionChanged: {
if (root.controller) {
root.controller.keyboardNavigationActive = false;
}
}
}
}

View File

@@ -16,7 +16,7 @@ const Weights = {
}
function tokenize(text) {
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function(w) { return w.length > 0 })
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 })
}
function hasWordBoundaryMatch(text, query) {
@@ -164,7 +164,7 @@ function scoreItems(items, query, getFrecencyFn) {
}
}
scored.sort(function(a, b) {
scored.sort(function (a, b) {
return b.score - a.score
})
@@ -204,7 +204,7 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
var section = sections[sectionOrder[i].id]
if (section && section.items.length > 0) {
if (sortAlphabetically && section.id === "apps") {
section.items.sort(function(a, b) {
section.items.sort(function (a, b) {
return (a.name || "").localeCompare(b.name || "")
})
}

View File

@@ -1,7 +1,9 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
@@ -21,9 +23,22 @@ Rectangle {
border.width: isSelected ? 2 : 0
border.color: Theme.primary
readonly property string toplevelId: item?.data?.toplevelId ?? ""
readonly property var waylandToplevel: {
if (!toplevelId || !item?.pluginId)
return null;
const pluginInstance = PluginService.pluginInstances[item.pluginId];
if (!pluginInstance?.getToplevelById)
return null;
return pluginInstance.getToplevelById(toplevelId);
}
readonly property bool hasScreencopy: waylandToplevel !== null
readonly property string iconValue: {
if (!item)
return "";
if (hasScreencopy)
return "";
var data = item.data;
if (data?.imageUrl)
return "image:" + data.imageUrl;
@@ -63,12 +78,26 @@ Rectangle {
color: Theme.surfaceContainerHigh
clip: true
ScreencopyView {
id: screencopyView
anchors.fill: parent
captureSource: root.waylandToplevel
live: root.hasScreencopy
visible: root.hasScreencopy
Rectangle {
anchors.fill: parent
color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent"
}
}
AppIconRenderer {
anchors.fill: parent
iconValue: root.iconValue
iconSize: Math.min(parent.width, parent.height)
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
materialIconSizeAdjustment: iconSize * 0.3
visible: !root.hasScreencopy
}
Rectangle {
@@ -110,16 +139,26 @@ Rectangle {
}
}
Image {
Rectangle {
id: attributionBadge
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Theme.spacingXS
width: 40
height: 16
fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || ""
visible: source !== ""
opacity: 0.9
width: root.hasScreencopy ? 28 : 40
height: root.hasScreencopy ? 28 : 16
radius: root.hasScreencopy ? 14 : 4
color: root.hasScreencopy ? Theme.surfaceContainer : "transparent"
visible: attributionImage.status === Image.Ready
opacity: 0.95
Image {
id: attributionImage
anchors.fill: parent
anchors.margins: root.hasScreencopy ? 4 : 0
fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || ""
mipmap: true
}
}
}
}

View File

@@ -508,18 +508,17 @@ Item {
function getRealWorkspaces() {
return root.workspaceList.filter(ws => {
if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isNiri)
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1;
return ws !== -1;
if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isNiri)
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1;
return ws !== -1;
});
}
@@ -864,6 +863,60 @@ Item {
property bool loadedHasIcon: false
property var loadedIcons: []
readonly property int stableIconCount: {
if (!SettingsData.showWorkspaceApps || isPlaceholder)
return 0;
let targetWorkspaceId;
if (root.useExtWorkspace) {
targetWorkspaceId = modelData?.id || modelData?.name;
} else if (CompositorService.isNiri) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isHyprland) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
targetWorkspaceId = modelData?.num;
}
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
return 0;
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels;
const seen = {};
let groupedCount = 0;
let totalCount = 0;
for (let i = 0; i < wins.length; i++) {
const w = wins[i];
if (!w)
continue;
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
winWs = hyprToplevel?.workspace?.id;
}
if (winWs !== targetWorkspaceId)
continue;
totalCount++;
const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
if (!seen[appKey]) {
seen[appKey] = true;
groupedCount++;
}
}
return SettingsData.groupWorkspaceApps ? groupedCount : totalCount;
}
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
@@ -872,27 +925,29 @@ Item {
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
}
return 0;
}
readonly property real iconsExtraHeight: {
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
}
return 0;
}
readonly property real visualWidth: {
if (contentImplicitWidth <= 0) return baseWidth + iconsExtraWidth;
if (contentImplicitWidth <= 0)
return baseWidth + iconsExtraWidth;
const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
}
readonly property real visualHeight: {
if (contentImplicitHeight <= 0) return baseHeight + iconsExtraHeight;
if (contentImplicitHeight <= 0)
return baseHeight + iconsExtraHeight;
const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS;
return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
}
@@ -1080,6 +1135,20 @@ Item {
width: root.isVertical ? root.widgetHeight : visualWidth
height: root.isVertical ? visualHeight : root.widgetHeight
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle {
id: focusedBorderRing
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
@@ -1349,6 +1418,15 @@ Item {
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
StyledText {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
anchors.horizontalCenter: parent.horizontalCenter
text: root.getWorkspaceIndex(modelData, index)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)

View File

@@ -125,6 +125,19 @@ Item {
return Theme.warning;
}
function formatThemeAutoTime(isoString) {
if (!isoString)
return "";
try {
const date = new Date(isoString);
if (isNaN(date.getTime()))
return "";
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
} catch (e) {
return "";
}
}
Component.onCompleted: {
SettingsData.detectAvailableIconThemes();
SettingsData.detectAvailableCursorThemes();
@@ -152,7 +165,6 @@ Item {
}
themeColorsTab.templateDetection = detection;
} catch (e) {
console.warn("ThemeColorsTab: Failed to parse template check:", e);
}
}
}
@@ -962,6 +974,453 @@ Item {
}
}
SettingsCard {
tab: "theme"
tags: ["automatic", "color", "mode", "schedule", "sunrise", "sunset"]
title: I18n.tr("Automatic Color Mode")
settingKey: "automaticColorMode"
iconName: "schedule"
Column {
width: parent.width
spacing: Theme.spacingM
DankToggle {
id: themeModeAutoToggle
width: parent.width
text: I18n.tr("Enable Automatic Switching")
description: I18n.tr("Automatically switch between light and dark modes based on time or sunrise/sunset")
checked: SessionData.themeModeAutoEnabled
onToggled: checked => {
SessionData.setThemeModeAutoEnabled(checked);
}
Connections {
target: SessionData
function onThemeModeAutoEnabledChanged() {
themeModeAutoToggle.checked = SessionData.themeModeAutoEnabled;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoEnabled
DankToggle {
width: parent.width
text: I18n.tr("Share Gamma Control Settings")
description: I18n.tr("Use the same time and location settings as gamma control")
checked: SessionData.themeModeShareGammaSettings
onToggled: checked => {
SessionData.setThemeModeShareGammaSettings(checked);
}
}
Item {
width: parent.width
height: 45 + Theme.spacingM
DankTabBar {
id: themeModeTabBar
width: 200
height: 45
anchors.horizontalCenter: parent.horizontalCenter
model: [
{ "text": "Time", "icon": "access_time" },
{ "text": "Location", "icon": "place" }
]
Component.onCompleted: {
currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0;
Qt.callLater(updateIndicator);
}
onTabClicked: index => {
SessionData.setThemeModeAutoMode(index === 1 ? "location" : "time");
currentIndex = index;
}
Connections {
target: SessionData
function onThemeModeAutoModeChanged() {
themeModeTabBar.currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0;
Qt.callLater(themeModeTabBar.updateIndicator);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoMode === "time" && !SessionData.themeModeShareGammaSettings
Column {
spacing: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingM
StyledText {
text: ""
width: 80
height: 20
}
StyledText {
text: I18n.tr("Hour")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: I18n.tr("Minute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Dark Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 80
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeStartHour.toString()
options: {
var hours = [];
for (var i = 0; i < 24; i++) hours.push(i.toString());
return hours;
}
onValueChanged: value => {
SessionData.setThemeModeStartHour(parseInt(value));
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = [];
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'));
}
return minutes;
}
onValueChanged: value => {
SessionData.setThemeModeStartMinute(parseInt(value));
}
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Light Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 80
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeEndHour.toString()
options: {
var hours = [];
for (var i = 0; i < 24; i++) hours.push(i.toString());
return hours;
}
onValueChanged: value => {
SessionData.setThemeModeEndHour(parseInt(value));
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = [];
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'));
}
return minutes;
}
onValueChanged: value => {
SessionData.setThemeModeEndMinute(parseInt(value));
}
}
}
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light mode will be active from Light Start to Dark Start") : I18n.tr("Dark mode will be active from Dark Start to Light Start")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoMode === "location" && !SessionData.themeModeShareGammaSettings
DankToggle {
id: themeModeIpLocationToggle
width: parent.width
text: I18n.tr("Use IP Location")
description: I18n.tr("Automatically detect location based on IP address")
checked: SessionData.nightModeUseIPLocation || false
onToggled: checked => {
SessionData.setNightModeUseIPLocation(checked);
}
Connections {
target: SessionData
function onNightModeUseIPLocationChanged() {
themeModeIpLocationToggle.checked = SessionData.nightModeUseIPLocation;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: !SessionData.nightModeUseIPLocation
StyledText {
text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
Row {
spacing: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lat = parseFloat(text);
if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) {
SessionData.setLatitude(lat);
}
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lon = parseFloat(text);
if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) {
SessionData.setLongitude(lon);
}
}
}
}
}
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust theme mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light mode will be active from sunrise to sunset") : I18n.tr("Dark mode will be active from sunset to sunrise")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: SessionData.nightModeUseIPLocation || (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0)
}
}
StyledText {
width: parent.width
text: I18n.tr("Using shared settings from Gamma Control")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: SessionData.themeModeShareGammaSettings
}
Rectangle {
width: parent.width
height: statusRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Row {
id: statusRow
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width - Theme.spacingM * 2
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 8
height: 8
radius: 4
color: SessionData.themeModeAutoEnabled ? Theme.success : Theme.error
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Automation")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
StyledText {
text: SessionData.themeModeAutoEnabled ? I18n.tr("Enabled") : I18n.tr("Disabled")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingS
DankIcon {
name: SessionData.isLightMode ? "light_mode" : "dark_mode"
size: Theme.iconSizeMedium
color: SessionData.isLightMode ? "#FFA726" : "#7E57C2"
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light Mode") : I18n.tr("Dark Mode")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("Active")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
visible: SessionData.themeModeAutoEnabled && SessionData.themeModeNextTransition
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "schedule"
size: Theme.iconSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Next Transition")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: themeColorsTab.formatThemeAutoTime(SessionData.themeModeNextTransition)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
}
}
}
}
}
SettingsCard {
tab: "theme"
tags: ["light", "dark", "mode", "appearance"]

View File

@@ -884,4 +884,52 @@ Singleton {
return allItems;
}
function getPluginLauncherCategories(pluginId) {
if (typeof PluginService === "undefined")
return [];
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return [];
if (typeof instance.getCategories !== "function")
return [];
try {
return instance.getCategories() || [];
} catch (e) {
console.warn("AppSearchService: Error getting categories from plugin", pluginId, ":", e);
return [];
}
}
function setPluginLauncherCategory(pluginId, categoryId) {
if (typeof PluginService === "undefined")
return;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return;
if (typeof instance.setCategory !== "function")
return;
try {
instance.setCategory(categoryId);
} catch (e) {
console.warn("AppSearchService: Error setting category on plugin", pluginId, ":", e);
}
}
function pluginHasCategories(pluginId) {
if (typeof PluginService === "undefined")
return false;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return false;
return typeof instance.getCategories === "function";
}
}

View File

@@ -130,7 +130,7 @@ Singleton {
Component.onCompleted: {
root.userPreference = SettingsData.networkPreference;
lastConnectedVpnUuid = SettingsData.vpnLastConnected || "";
lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities();
}
@@ -293,7 +293,7 @@ Singleton {
if (vpnConnected && activeUuid) {
lastConnectedVpnUuid = activeUuid;
SettingsData.set("vpnLastConnected", activeUuid);
SessionData.setVpnLastConnected(activeUuid);
}
if (vpnIsBusy) {

View File

@@ -56,6 +56,7 @@ Singleton {
signal wlrOutputStateUpdate(var data)
signal evdevStateUpdate(var data)
signal gammaStateUpdate(var data)
signal themeAutoStateUpdate(var data)
signal openUrlRequested(string url)
signal appPickerRequested(var data)
signal screensaverStateUpdate(var data)
@@ -64,7 +65,7 @@ Singleton {
property bool screensaverInhibited: false
property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
@@ -304,7 +305,7 @@ Singleton {
excludeServices = [excludeServices];
}
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
const filtered = allServices.filter(s => !excludeServices.includes(s));
subscribe(filtered);
}
@@ -373,6 +374,8 @@ Singleton {
evdevStateUpdate(data);
} else if (service === "gamma") {
gammaStateUpdate(data);
} else if (service === "theme.auto") {
themeAutoStateUpdate(data);
} else if (service === "browser.open_requested") {
if (data.target) {
if (data.requestType === "url" || !data.requestType) {

View File

@@ -396,7 +396,6 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = delegateRoot.modelData;
root.valueChanged(delegateRoot.modelData);
dropdownMenu.close();
}