Compare commits

...
13 Commits
Author SHA1 Message Date
bbedward 84bb4d4753 add aqueous logo 2026-09-07 11:14:51 -04:00
bbedward f5acb716b9 aqueous: consolidate more aqueous specific stuff 2026-09-07 10:32:58 -04:00
bbedward b138ec1b0f fix staticcheck 2026-09-07 09:53:02 -04:00
bbedward 5a72cd9616 aqueous: move IPC integration to QML, delete test fixtures (since we
dont have a QML test structure even setup), delete in-repo docs
2026-09-07 09:08:46 -04:00
Zoey ce1ecfc6bc swapped to ipc 2026-09-06 23:07:29 -04:00
Zoey 80472c4db8 clean up per claud review 2026-09-06 16:12:34 -04:00
Zoey 49513047b2 remove unneeded blur 2026-09-06 15:23:30 -04:00
Zoey 045c5c3a89 added keybinding update 2026-09-06 02:33:50 -04:00
Zoey 398b360c48 added blur to dms shell for aqueous 2026-09-05 23:18:19 -04:00
Zoey a39196e992 updated to support workspace options better in aqueous 2026-09-05 20:55:09 -04:00
Zoey 2ac1fe670c aqueous support work 2026-09-05 20:39:19 -04:00
Zoey f39529d58c further updates 2026-09-05 17:03:57 -04:00
Zoey 8874a804b6 updated dank to support aqueous 2026-09-05 17:03:36 -04:00
60 changed files with 3861 additions and 227 deletions
+94 -79
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -46,7 +47,7 @@ var keybindsSetCmd = &cobra.Command{
Short: "Set a keybind override", Short: "Set a keybind override",
Long: "Create or update a keybind override for the specified provider", Long: "Create or update a keybind override for the specified provider",
Args: cobra.ExactArgs(3), Args: cobra.ExactArgs(3),
Run: runKeybindsSet, Run: runKeybindsEdit,
} }
var keybindsRemoveCmd = &cobra.Command{ var keybindsRemoveCmd = &cobra.Command{
@@ -54,7 +55,7 @@ var keybindsRemoveCmd = &cobra.Command{
Short: "Remove a keybind", Short: "Remove a keybind",
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.", Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: runKeybindsRemove, Run: runKeybindsEdit,
} }
var keybindsResetCmd = &cobra.Command{ var keybindsResetCmd = &cobra.Command{
@@ -62,10 +63,14 @@ var keybindsResetCmd = &cobra.Command{
Short: "Reset a keybind override to its DMS default", Short: "Reset a keybind override to its DMS default",
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.", Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: runKeybindsReset, Run: runKeybindsEdit,
} }
func init() { func init() {
keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings")
for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} {
command.Flags().Bool("json", false, "Return structured mutation results, including errors")
}
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON") keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider") keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay") keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
@@ -93,6 +98,9 @@ func initializeProviders() {
registry := keybinds.GetDefaultRegistry() registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("") hyprlandProvider := providers.NewHyprlandProvider("")
if err := registry.Register(providers.NewAqueousProvider()); err != nil {
log.Warnf("Failed to register Aqueous provider: %v", err)
}
if err := registry.Register(hyprlandProvider); err != nil { if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err) log.Warnf("Failed to register Hyprland provider: %v", err)
} }
@@ -206,85 +214,92 @@ func runKeybindsShow(cmd *cobra.Command, args []string) {
printCheatSheet(provider) printCheatSheet(provider)
} }
func getWritableProvider(name string) keybinds.WritableProvider { func keybindEditFailure(err error) map[string]any {
provider, err := keybinds.GetDefaultRegistry().Get(name) code := "command_failed"
var helperError *providers.AqueousError
if errors.As(err, &helperError) {
code = helperError.Code
}
return map[string]any{"success": false, "code": code, "message": err.Error()}
}
func runKeybindsEdit(cmd *cobra.Command, args []string) {
result, err := editKeybind(cmd, args)
if err == nil {
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(result)
return
}
structured, _ := cmd.Flags().GetBool("json")
if !structured {
log.Fatalf("Failed to save keybind: %v", err)
return
}
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(keybindEditFailure(err))
os.Exit(1)
}
func editKeybind(cmd *cobra.Command, args []string) (map[string]any, error) {
provider, err := keybinds.GetDefaultRegistry().Get(args[0])
if err != nil { if err != nil {
log.Fatalf("Error: %v", err) return nil, err
}
key := args[1]
if aqueous, ok := provider.(*providers.AqueousProvider); ok {
generation, _ := cmd.Flags().GetString("expected-generation")
edit := providers.AqueousBindEdit{Generation: generation, Key: key, Remove: cmd.Name() != "set"}
if !edit.Remove {
for _, flag := range []string{"desc", "allow-when-locked", "cooldown-ms", "no-repeat", "no-inhibiting", "flags"} {
if cmd.Flags().Changed(flag) {
return nil, fmt.Errorf("aqueous does not support --%s in this provider", flag)
}
}
edit.Action = args[2]
edit.OriginalKey, _ = cmd.Flags().GetString("replace-key")
}
result, err := aqueous.Edit(cmd.Context(), edit)
if err != nil {
return nil, err
}
return map[string]any{"success": true, "code": "applied", "generation": result.String("generation"), "key": key}, nil
} }
writable, ok := provider.(keybinds.WritableProvider) writable, ok := provider.(keybinds.WritableProvider)
if !ok { if !ok {
log.Fatalf("Provider %s does not support writing keybinds", name) return nil, fmt.Errorf("provider %s does not support writing keybinds", args[0])
} }
return writable result := map[string]any{"success": true, "key": key}
} switch cmd.Name() {
case "set":
func runKeybindsSet(cmd *cobra.Command, args []string) { options := make(map[string]any)
providerName, key, action := args[0], args[1], args[2] if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
writable := getWritableProvider(providerName) options["allow-when-locked"] = true
}
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key { if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
_ = writable.RemoveBind(replaceKey) options["cooldown-ms"] = v
} }
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options := make(map[string]any) options["repeat"] = false
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v { }
options["allow-when-locked"] = true if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
} options["allow-inhibiting"] = false
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 { }
options["cooldown-ms"] = v if v, _ := cmd.Flags().GetString("flags"); v != "" {
} options["flags"] = v
if v, _ := cmd.Flags().GetBool("no-repeat"); v { }
options["repeat"] = false if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
} _ = writable.RemoveBind(replaceKey)
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v { }
options["allow-inhibiting"] = false desc, _ := cmd.Flags().GetString("desc")
} err = writable.SetBind(key, args[2], desc, options)
if v, _ := cmd.Flags().GetString("flags"); v != "" { result["action"] = args[2]
options["flags"] = v result["path"] = writable.GetOverridePath()
} case "remove":
err = writable.RemoveBind(key)
desc, _ := cmd.Flags().GetString("desc") result["removed"] = true
if err := writable.SetBind(key, action, desc, options); err != nil { case "reset":
log.Fatalf("Error setting keybind: %v", err) err = writable.ResetBind(key)
} result["reset"] = true
default:
output, _ := json.MarshalIndent(map[string]any{ return nil, fmt.Errorf("unsupported keybind operation: %s", cmd.Name())
"success": true, }
"key": key, return result, err
"action": action,
"path": writable.GetOverridePath(),
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsRemove(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.RemoveBind(key); err != nil {
log.Fatalf("Error removing keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"removed": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsReset(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.ResetBind(key); err != nil {
log.Fatalf("Error resetting keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"reset": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
} }
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"errors"
"fmt"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
)
func TestKeybindEditFailure(t *testing.T) {
for _, tc := range []struct {
err error
code string
}{
{&providers.AqueousError{Code: "external_change", Message: "changed"}, "external_change"},
{fmt.Errorf("helper: %w", &providers.AqueousError{Code: "read_only", Message: "denied"}), "read_only"},
{errors.New("transport failed"), "command_failed"},
} {
result := keybindEditFailure(tc.err)
if result["success"] != false || result["code"] != tc.code || result["message"] != tc.err.Error() {
t.Fatalf("unexpected failure result: %#v", result)
}
}
}
+6 -3
View File
@@ -20,6 +20,7 @@ import (
var ( var (
ssOutputName string ssOutputName string
ssSeat string
ssCursor string ssCursor string
ssFormat string ssFormat string
ssQuality int ssQuality int
@@ -58,7 +59,7 @@ Modes:
full - Capture the focused output full - Capture the focused output
all - Capture all outputs combined all - Capture all outputs combined
output - Capture a specific output by name output - Capture a specific output by name
window - Capture the focused window (Hyprland/Mango/niri) window - Capture the focused window (Hyprland/Mango/niri/Aqueous)
last - Capture the last selected region last - Capture the last selected region
scroll - Select a region, then scroll to capture a stitched tall image scroll - Select a region, then scroll to capture a stitched tall image
@@ -72,7 +73,7 @@ Examples:
dms screenshot full # Full screen of focused output dms screenshot full # Full screen of focused output
dms screenshot all # All screens combined dms screenshot all # All screens combined
dms screenshot output -o DP-1 # Specific output dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland) dms screenshot window # Focused window
dms screenshot last # Last region (pre-selected) dms screenshot last # Last region (pre-selected)
dms screenshot --reset # Reset last region pre-selection dms screenshot --reset # Reset last region pre-selection
dms screenshot --no-clipboard # Save file only dms screenshot --no-clipboard # Save file only
@@ -122,7 +123,7 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{ var ssWindowCmd = &cobra.Command{
Use: "window", Use: "window",
Short: "Capture the focused window", Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland, Mango, and niri.`, Long: `Capture the currently focused window. Supported on Hyprland, Mango, niri, and Aqueous. Aqueous requires a running DMS shell and crops the output including borders and overlapping windows.`,
Run: runScreenshotWindow, Run: runScreenshotWindow,
} }
@@ -165,6 +166,7 @@ var notifyActionCmd = &cobra.Command{
func init() { func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode") screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().StringVar(&ssSeat, "seat", "", "Seat for Aqueous capture (required with multiple seats)")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)") screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)") screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)") screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
@@ -198,6 +200,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig() config := screenshot.DefaultConfig()
config.Mode = mode config.Mode = mode
config.OutputName = ssOutputName config.OutputName = ssOutputName
config.Seat = ssSeat
if strings.EqualFold(ssCursor, "on") { if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn config.Cursor = screenshot.CursorOn
} }
+178
View File
@@ -0,0 +1,178 @@
package providers
import (
"context"
"errors"
"fmt"
"slices"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
)
type AqueousProvider struct{}
func NewAqueousProvider() *AqueousProvider { return &AqueousProvider{} }
func (*AqueousProvider) Name() string { return "aqueous" }
func aqueousObjects(snapshot aqueousConfig, key string) []aqueousConfig {
values, _ := snapshot[key].([]any)
result := make([]aqueousConfig, 0, len(values))
for _, value := range values {
if object, ok := value.(map[string]any); ok {
result = append(result, aqueousConfig(object))
}
}
return result
}
func AqueousCheatSheet(snapshot aqueousConfig) (*keybinds.CheatSheet, error) {
if err := requireAqueousCapabilities(snapshot, "keybinds"); err != nil {
return nil, err
}
sheet := &keybinds.CheatSheet{Title: "Aqueous", Provider: "aqueous", Generation: snapshot.String("generation"), ModKey: "Super", DMSBindsIncluded: true, Binds: map[string][]keybinds.Keybind{"Compositor": {}, "Custom": {}}}
for _, field := range aqueousObjects(snapshot, "fields") {
if field.String("category") != "keybinds" || field.String("type") != "string_list" {
continue
}
values, _ := field["value"].([]any)
if len(values) == 0 {
values = []any{""}
}
for _, value := range values {
key, ok := value.(string)
if !ok {
return nil, errors.New("invalid keybinding list")
}
sheet.Binds["Compositor"] = append(sheet.Binds["Compositor"], keybinds.Keybind{Key: key, Action: field.String("id"), Description: field.String("label"), Source: "dms"})
}
}
for _, binding := range aqueousObjects(snapshot, "custom_keybinds") {
sheet.Binds["Custom"] = append(sheet.Binds["Custom"], keybinds.Keybind{Key: binding.String("chord"), Action: "spawn " + binding.String("command"), Description: binding.String("command"), Source: "dms"})
}
seen := map[string]keybinds.Keybind{}
for _, category := range []string{"Compositor", "Custom"} {
for i, bind := range sheet.Binds[category] {
if bind.Key == "" {
continue
}
if previous, exists := seen[bind.Key]; exists {
sheet.Binds[category][i].Conflict = &previous
}
seen[bind.Key] = bind
}
}
return sheet, nil
}
func (*AqueousProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
snapshot, err := aqueousHelper(context.Background(), "snapshot", nil)
if err != nil {
return nil, err
}
return AqueousCheatSheet(snapshot)
}
type AqueousBindEdit struct {
Generation string
OriginalKey string
Key string
Action string
Remove bool
}
func AqueousBindRequest(snapshot aqueousConfig, edit AqueousBindEdit) (aqueousConfig, error) {
if edit.Generation == "" || edit.Generation != snapshot.String("generation") {
return nil, &AqueousError{Code: "external_change", Message: "reload and reconcile the retained keybind draft"}
}
if err := requireAqueousCapabilities(snapshot, "keybinds"); err != nil {
return nil, err
}
if edit.Key == "" {
return nil, errors.New("missing key")
}
original := edit.OriginalKey
if original == "" {
original = edit.Key
}
originalCount := 0
for _, field := range aqueousObjects(snapshot, "fields") {
if field.String("category") != "keybinds" || field.String("type") != "string_list" {
continue
}
values, _ := field["value"].([]any)
for _, value := range values {
if value == original {
originalCount++
}
}
}
for _, binding := range aqueousObjects(snapshot, "custom_keybinds") {
if binding.String("chord") == original {
originalCount++
}
}
if originalCount > 1 {
return nil, &AqueousError{Code: "ambiguous_target", Message: "multiple bindings use the original shortcut"}
}
if originalCount == 0 && (edit.Remove || edit.OriginalKey != "") {
return nil, &AqueousError{Code: "target_removed", Message: "the original shortcut no longer exists"}
}
changes := []any{}
customChanges := []any{}
foundAction := false
for _, field := range aqueousObjects(snapshot, "fields") {
if field.String("category") != "keybinds" || field.String("type") != "string_list" {
continue
}
old, _ := field["value"].([]any)
values := make([]any, 0, len(old)+1)
for _, value := range old {
if value == original {
continue
}
if !edit.Remove && value == edit.Key && field.String("id") != edit.Action {
return nil, fmt.Errorf("key conflict: %s", edit.Key)
}
values = append(values, value)
}
if field.String("id") == edit.Action && !edit.Remove {
foundAction = true
if !slices.Contains(values, any(edit.Key)) {
values = append(values, edit.Key)
}
}
if !slices.Equal(old, values) {
changes = append(changes, map[string]any{"id": field.String("id"), "value": values})
}
}
for _, binding := range aqueousObjects(snapshot, "custom_keybinds") {
if binding.String("chord") == original {
customChanges = append(customChanges, map[string]any{"op": "delete", "id": binding.String("id")})
continue
}
if !edit.Remove && binding.String("chord") == edit.Key {
return nil, fmt.Errorf("key conflict: %s", edit.Key)
}
}
if !edit.Remove && !foundAction {
command, ok := strings.CutPrefix(edit.Action, "spawn ")
if !ok || strings.TrimSpace(command) == "" {
return nil, errors.New("unknown Aqueous action; custom commands use spawn")
}
customChanges = append(customChanges, map[string]any{"op": "add", "chord": edit.Key, "command": command})
}
return aqueousConfig{"expected_generation": edit.Generation, "changes": changes, "custom_keybind_changes": customChanges, "create_user_override": true}, nil
}
func (*AqueousProvider) Edit(ctx context.Context, edit AqueousBindEdit) (aqueousConfig, error) {
snapshot, err := aqueousHelper(ctx, "snapshot", nil)
if err != nil {
return nil, err
}
request, err := AqueousBindRequest(snapshot, edit)
if err != nil {
return nil, err
}
return aqueousHelper(ctx, "apply", request)
}
@@ -0,0 +1,115 @@
package providers
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func helperResult(ctx context.Context, verb string, input []byte) (aqueousConfig, error) {
var result aqueousConfig
args := []string{verb, "--shell", "dms"}
if input != nil {
args = append(args, "--request", "-")
}
err := utils.RunJSON(ctx, "aqueous-config", args, input, &result)
if err != nil || !result.Bool("ok") {
if result.String("code") != "" {
return result, &AqueousError{Code: result.String("code"), Message: result.String("message")}
}
if err != nil {
if verb == "apply" {
return result, &AqueousError{Code: "uncertain", Message: err.Error()}
}
return result, err
}
return result, errors.New("aqueous-config rejected the request")
}
return result, nil
}
type AqueousError struct {
Code string
Message string
}
func (e *AqueousError) Error() string { return e.Code + ": " + e.Message }
func requireAqueousCapabilities(result aqueousConfig, required ...string) error {
if result.Number("protocol") != 1 {
return errors.New("unsupported aqueous-config protocol")
}
values, ok := result["capabilities"].([]any)
if !ok {
return errors.New("unsupported aqueous-config: capability discovery required")
}
for _, capability := range required {
if !slices.Contains(values, any(capability)) {
return fmt.Errorf("unsupported aqueous-config capability: %s", capability)
}
}
return nil
}
func aqueousHelper(ctx context.Context, verb string, request aqueousConfig) (aqueousConfig, error) {
if verb == "snapshot" {
result, err := helperResult(ctx, verb, nil)
if err != nil {
return result, err
}
if err := requireAqueousCapabilities(result, "schema_fields", "shell_dms"); err != nil {
return result, err
}
if result.String("generation") == "" {
return result, errors.New("missing configuration generation")
}
return result, nil
}
if verb != "validate" && verb != "apply" {
return nil, errors.New("unsupported helper operation")
}
if request.String("expected_generation") == "" {
return nil, errors.New("missing expected_generation; reload configuration")
}
version, err := helperResult(ctx, "version", nil)
if err != nil {
return nil, err
}
required := []string{"keybinds", "validate", "stdin_requests", "generation_check", "atomic_file_replace", "shell_dms"}
if err := requireAqueousCapabilities(version, required...); err != nil {
return nil, err
}
request["protocol"] = 1
if _, exists := request["backup_dir"]; !exists {
configDir, err := os.UserConfigDir()
if err != nil {
return nil, err
}
request["backup_dir"] = filepath.Join(configDir, "DankMaterialShell", "aqueous-backups")
}
input, err := json.Marshal(request)
if err != nil {
return nil, err
}
validated, err := helperResult(ctx, "validate", input)
if err != nil || verb == "validate" {
return validated, err
}
return helperResult(ctx, "apply", input)
}
type aqueousConfig map[string]any
func (e aqueousConfig) String(key string) string { value, _ := e[key].(string); return value }
func (e aqueousConfig) Bool(key string) bool { value, _ := e[key].(bool); return value }
func (e aqueousConfig) Number(key string) float64 {
value, _ := e[key].(json.Number)
number, _ := value.Float64()
return number
}
@@ -0,0 +1,133 @@
package providers
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func aqueousFixture(t *testing.T) aqueousConfig {
t.Helper()
var snapshot aqueousConfig
err := utils.DecodeJSON([]byte(`{"ok":true,"protocol":1,"generation":"abc","capabilities":["keybinds"],"fields":[{"id":"spawn_terminal","category":"keybinds","type":"string_list","label":"Terminal","value":["Super+Return","Super+T"]},{"id":"close","category":"keybinds","type":"string_list","label":"Close","value":[]}],"custom_keybinds":[{"id":"custom:12","chord":"Super+R","command":"printf 'hello; $world'"}]}`), &snapshot)
if err != nil {
t.Fatal(err)
}
return snapshot
}
func TestAqueousCheatsheetIncludesUnboundAndCustom(t *testing.T) {
sheet, err := AqueousCheatSheet(aqueousFixture(t))
if err != nil {
t.Fatal(err)
}
if sheet.Generation != "abc" || len(sheet.Binds["Compositor"]) != 3 {
t.Fatal("lost generation or multiple bindings")
}
if sheet.Binds["Compositor"][2].Key != "" || sheet.Binds["Compositor"][2].Action != "close" {
t.Fatal("unbound action lost")
}
if sheet.Binds["Custom"][0].Action != "spawn printf 'hello; $world'" {
t.Fatal("custom command modified")
}
}
func TestAqueousBindingReplacementIsOneRequest(t *testing.T) {
snapshot := aqueousFixture(t)
request, err := AqueousBindRequest(snapshot, AqueousBindEdit{Generation: "abc", OriginalKey: "Super+Return", Key: "Super+Enter", Action: "spawn_terminal"})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(request["changes"])
if string(data) != `[{"id":"spawn_terminal","value":["Super+T","Super+Enter"]}]` {
t.Fatalf("wrong atomic replacement: %s", data)
}
if request["expected_generation"] != "abc" {
t.Fatal("generation replaced")
}
if !reflect.DeepEqual(snapshot, aqueousFixture(t)) {
t.Fatal("draft mutated snapshot")
}
}
func TestAqueousBindingConflictAndRemove(t *testing.T) {
for _, edit := range []AqueousBindEdit{
{Generation: "stale", Key: "Super+T", Remove: true},
{Generation: "abc", OriginalKey: "Super+Return", Key: "Super+R", Action: "close"},
{Generation: "abc", Key: "Super+Q", Action: "invented_action"},
} {
if _, err := AqueousBindRequest(aqueousFixture(t), edit); err == nil {
t.Fatal("invalid edit accepted")
}
}
request, err := AqueousBindRequest(aqueousFixture(t), AqueousBindEdit{Generation: "abc", Key: "Super+R", Remove: true})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(request["custom_keybind_changes"])
if string(data) != `[{"id":"custom:12","op":"delete"}]` {
t.Fatalf("wrong removal: %s", data)
}
}
func TestAqueousGenerationConflictCode(t *testing.T) {
for _, generation := range []string{"", "stale"} {
_, err := AqueousBindRequest(aqueousFixture(t), AqueousBindEdit{Generation: generation, Key: "Super+R", Remove: true})
var conflict *AqueousError
if !errors.As(err, &conflict) || conflict.Code != "external_change" {
t.Fatalf("expected structured generation conflict, got %v", err)
}
}
}
func TestAqueousBindingRequiresUniqueOriginal(t *testing.T) {
for _, edit := range []AqueousBindEdit{
{Generation: "abc", Key: "Super+Missing", Remove: true},
{Generation: "abc", OriginalKey: "Super+Missing", Key: "Super+X", Action: "spawn_terminal"},
} {
_, err := AqueousBindRequest(aqueousFixture(t), edit)
var conflict *AqueousError
if !errors.As(err, &conflict) || conflict.Code != "target_removed" {
t.Fatalf("expected missing original, got %v", err)
}
}
snapshot := aqueousFixture(t)
snapshot["custom_keybinds"] = append(snapshot["custom_keybinds"].([]any), map[string]any{"id": "duplicate", "chord": "Super+T", "command": "echo duplicate"})
_, err := AqueousBindRequest(snapshot, AqueousBindEdit{Generation: "abc", Key: "Super+T", Remove: true})
var conflict *AqueousError
if !errors.As(err, &conflict) || conflict.Code != "ambiguous_target" {
t.Fatalf("expected ambiguous original, got %v", err)
}
}
func TestAqueousHelperFailureCodes(t *testing.T) {
for _, tc := range []struct {
name string
script string
code string
}{
{"conflict", `printf '%s' '{"ok":false,"code":"external_change","message":"changed"}'; exit 1`, "external_change"},
{"missing_ack", "exit 0", "uncertain"},
{"failed_process", "exit 1", "uncertain"},
{"malformed_ack", "printf '{'", "uncertain"},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "aqueous-config"), []byte("#!/bin/sh\n"+tc.script+"\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
_, err := helperResult(context.Background(), "apply", nil)
var failure *AqueousError
if !errors.As(err, &failure) || failure.Code != tc.code {
t.Fatalf("expected %s, got %v", tc.code, err)
}
})
}
}
+1
View File
@@ -30,6 +30,7 @@ type DMSBindsStatus struct {
} }
type CheatSheet struct { type CheatSheet struct {
Generation string `json:"generation,omitempty"`
Title string `json:"title"` Title string `json:"title"`
Provider string `json:"provider"` Provider string `json:"provider"`
ModKey string `json:"modKey,omitempty"` ModKey string `json:"modKey,omitempty"`
+95
View File
@@ -0,0 +1,95 @@
package screenshot
import (
"context"
"errors"
"image"
"math"
)
func aqueousWindowGeometry(model aqueousSnapshotModel, seatName string) (*WindowGeometry, error) {
seat, err := model.Seat(seatName)
if err != nil {
return nil, err
}
if seat.FocusKind != "window" {
return nil, errors.New("no active window: seat focus is not a window")
}
window := model.Entities["window:"+seat.Window]
if window == nil || !window.Visible || window.Minimized || !window.CanActivate {
return nil, errors.New("no eligible active window")
}
output := model.Entities["output:"+window.Output]
if output == nil || !output.Enabled || !output.Powered {
return nil, errors.New("active window output is unavailable")
}
box, bounds := window.OuterGeometry, output.Bounds
if box == nil || bounds == nil || !box.valid() || !bounds.valid() || box.Width <= 0 || box.Height <= 0 || output.Scale <= 0 || math.IsInf(output.Scale, 0) || math.IsNaN(output.Scale) {
return nil, errors.New("invalid active window geometry")
}
return &WindowGeometry{X: int32(box.X), Y: int32(box.Y), Width: int32(box.Width), Height: int32(box.Height), Output: output.Name, Scale: output.Scale, OutputX: int32(bounds.X), OutputY: int32(bounds.Y)}, nil
}
func aqueousCropRect(geom *WindowGeometry, pixels image.Rectangle) image.Rectangle {
left := math.Floor((float64(geom.X) - float64(geom.OutputX)) * geom.Scale)
top := math.Floor((float64(geom.Y) - float64(geom.OutputY)) * geom.Scale)
right := math.Ceil((float64(geom.X) + float64(geom.Width) - float64(geom.OutputX)) * geom.Scale)
bottom := math.Ceil((float64(geom.Y) + float64(geom.Height) - float64(geom.OutputY)) * geom.Scale)
return image.Rect(int(left), int(top), int(right), int(bottom)).Intersect(pixels)
}
func aqueousFocusedOutput(seatName string) (string, error) {
model, err := aqueousSnapshot(context.Background())
if err != nil {
return "", err
}
seat, err := model.Seat(seatName)
if err != nil {
return "", err
}
output := model.Entities["output:"+seat.Output]
if output == nil || !output.Enabled || !output.Powered {
return "", errors.New("selected output is unavailable")
}
return output.Name, nil
}
func (s *Screenshoter) captureAqueousWindow() (*CaptureResult, error) {
model, err := aqueousSnapshot(context.Background())
if err != nil {
return nil, err
}
geom, err := aqueousWindowGeometry(model, s.config.Seat)
if err != nil {
return nil, err
}
region := Region{X: geom.X, Y: geom.Y, Width: geom.Width, Height: geom.Height, Output: geom.Output}
if s.config.Geometry {
return geometryResult(region), nil
}
output := s.findOutputByName(geom.Output)
if output == nil {
return nil, errors.New("active window output disappeared")
}
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
}
defer result.Buffer.Close()
rect := aqueousCropRect(geom, image.Rect(0, 0, result.Buffer.Width, result.Buffer.Height))
if rect.Empty() {
return nil, errors.New("active window is outside the output")
}
bpp := PixelFormat(result.Format).BytesPerPixel()
cropped, err := CreateShmBuffer(rect.Dx(), rect.Dy(), rect.Dx()*bpp)
if err != nil {
return nil, err
}
src, dst := result.Buffer.Data(), cropped.Data()
for y := 0; y < rect.Dy(); y++ {
start := (rect.Min.Y+y)*result.Buffer.Stride + rect.Min.X*bpp
copy(dst[y*cropped.Stride:y*cropped.Stride+rect.Dx()*bpp], src[start:start+rect.Dx()*bpp])
}
cropped.Format = PixelFormat(result.Format)
return &CaptureResult{Buffer: cropped, Region: region, Format: result.Format, Scale: geom.Scale, CICP: result.CICP}, nil
}
@@ -0,0 +1,119 @@
package screenshot
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"regexp"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func aqueousSnapshot(ctx context.Context) (aqueousSnapshotModel, error) {
executable, err := os.Executable()
if err != nil {
return aqueousSnapshotModel{}, err
}
var data json.RawMessage
if err := utils.RunJSON(ctx, executable, []string{"ipc", "call", "aqueous", "snapshot", os.Getenv("AQUEOUS_SOCKET")}, nil, &data); err != nil {
return aqueousSnapshotModel{}, fmt.Errorf("get Aqueous state from DMS; a shell must be running in this session: %w", err)
}
return parseAqueousSnapshot(data)
}
type aqueousBox struct {
X, Y, Width, Height int64
}
func (b aqueousBox) valid() bool {
for _, value := range []int64{b.X, b.Y, b.Width, b.Height} {
if value < math.MinInt32 || value > math.MaxInt32 {
return false
}
}
return true
}
type aqueousCaptureEntity struct {
Kind string `json:"kind"`
ID string `json:"id"`
Name string `json:"name"`
Output string `json:"output"`
Window string `json:"window"`
FocusKind string `json:"focus_kind"`
Visible bool `json:"visible"`
Minimized bool `json:"minimized"`
CanActivate bool `json:"can_activate"`
Enabled bool `json:"enabled"`
Powered bool `json:"powered"`
Scale float64 `json:"scale"`
Bounds *aqueousBox `json:"bounds"`
OuterGeometry *aqueousBox `json:"outer_geometry"`
}
type aqueousSnapshotModel struct {
Entities map[string]*aqueousCaptureEntity
}
func (m aqueousSnapshotModel) Seat(name string) (*aqueousCaptureEntity, error) {
if name != "" {
if seat := m.Entities["seat:"+name]; seat != nil {
return seat, nil
}
return nil, errors.New("not_found: seat")
}
var seat *aqueousCaptureEntity
for _, entity := range m.Entities {
if entity.Kind != "seat" {
continue
}
if seat != nil {
return nil, errors.New("ambiguous_seat")
}
seat = entity
}
if seat == nil {
return nil, errors.New("unavailable: no seat")
}
return seat, nil
}
func parseAqueousSnapshot(data []byte) (aqueousSnapshotModel, error) {
var batch struct {
Error string `json:"error"`
Schema int `json:"schema"`
Session string `json:"session"`
Sequence string `json:"sequence"`
BaseSequence json.RawMessage `json:"base_sequence"`
Type string `json:"type"`
Upsert []aqueousCaptureEntity `json:"upsert"`
Removed []string `json:"removed"`
}
var model aqueousSnapshotModel
if err := utils.DecodeJSON(data, &batch); err != nil {
return model, err
}
if batch.Error != "" {
return model, errors.New("aqueous state is unavailable in the running DMS shell")
}
if batch.Schema != 1 || batch.Type != "snapshot" || !bytes.Equal(batch.BaseSequence, []byte("null")) || batch.Upsert == nil || batch.Removed == nil || len(batch.Removed) != 0 || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(batch.Session) || !regexp.MustCompile(`^[0-9]+$`).MatchString(batch.Sequence) {
return model, errors.New("invalid Aqueous shell snapshot")
}
model.Entities = make(map[string]*aqueousCaptureEntity, len(batch.Upsert))
for i := range batch.Upsert {
entity := &batch.Upsert[i]
key := entity.Kind + ":" + entity.ID
if entity.ID == "" || entity.Kind == "" || model.Entities[key] != nil {
return aqueousSnapshotModel{}, errors.New("invalid or duplicate shell entity")
}
model.Entities[key] = entity
}
if model.Entities["session:session"] == nil {
return aqueousSnapshotModel{}, errors.New("missing session entity")
}
return model, nil
}
+79
View File
@@ -0,0 +1,79 @@
package screenshot
import (
"bytes"
"image"
"os"
"testing"
)
func TestAqueousCropRoundsOutwardAndClips(t *testing.T) {
for _, test := range []struct {
geometry WindowGeometry
pixels image.Rectangle
want image.Rectangle
}{
{WindowGeometry{X: -1919, Y: 1, Width: 101, Height: 51, OutputX: -1920, Scale: 1.25}, image.Rect(0, 0, 2400, 1350), image.Rect(1, 1, 128, 65)},
{WindowGeometry{X: -20, Y: -10, Width: 100, Height: 60, Scale: 1.5}, image.Rect(0, 0, 800, 600), image.Rect(0, 0, 120, 75)},
{WindowGeometry{X: 90, Y: 80, Width: 100, Height: 100, Scale: 1}, image.Rect(0, 0, 100, 100), image.Rect(90, 80, 100, 100)},
{WindowGeometry{X: 200, Y: 200, Width: 20, Height: 20, Scale: 1}, image.Rect(0, 0, 100, 100), image.Rectangle{}},
} {
if got := aqueousCropRect(&test.geometry, test.pixels); got != test.want {
t.Fatalf("got %v want %v", got, test.want)
}
}
}
func TestAqueousGeometryUsesSeatAndOuterBounds(t *testing.T) {
data, err := os.ReadFile("testdata/aqueous-snapshot.json")
if err != nil {
t.Fatal(err)
}
model, err := parseAqueousSnapshot(data)
if err != nil {
t.Fatal(err)
}
geometry, err := aqueousWindowGeometry(model, "")
if err != nil {
t.Fatal(err)
}
seat, _ := model.Seat("")
window := model.Entities["window:"+seat.Window]
if geometry.Width != int32(window.OuterGeometry.Width) || geometry.X != int32(window.OuterGeometry.X) {
t.Fatal("wrong bounds")
}
seat.FocusKind = "layer_surface"
if _, err := aqueousWindowGeometry(model, ""); err == nil {
t.Fatal("layer focus captured")
}
seat.FocusKind = "window"
model.Entities["seat:second"] = &aqueousCaptureEntity{Kind: "seat", ID: "second"}
if _, err := aqueousWindowGeometry(model, ""); err == nil {
t.Fatal("ambiguous seat captured")
}
if _, err := aqueousWindowGeometry(model, seat.ID); err != nil {
t.Fatal(err)
}
delete(model.Entities, "window:"+seat.Window)
if _, err := aqueousWindowGeometry(model, seat.ID); err == nil {
t.Fatal("removed window captured")
}
}
func TestAqueousSnapshotRejectsInvalidInput(t *testing.T) {
data, err := os.ReadFile("testdata/aqueous-snapshot.json")
if err != nil {
t.Fatal(err)
}
for _, input := range [][]byte{
[]byte(`{}`),
bytes.Replace(data, []byte(`"schema": 1`), []byte(`"schema": 2`), 1),
bytes.Replace(data, []byte(`"type": "snapshot"`), []byte(`"type": "delta"`), 1),
append(data, []byte(`{}`)...),
{0xff},
} {
if _, err := parseAqueousSnapshot(input); err == nil {
t.Fatal("invalid snapshot accepted")
}
}
}
+13 -1
View File
@@ -1,6 +1,7 @@
package screenshot package screenshot
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@@ -21,6 +22,7 @@ const (
CompositorScroll CompositorScroll
CompositorMiracle CompositorMiracle
CompositorMango CompositorMango
CompositorAqueous
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -35,6 +37,7 @@ func DetectCompositor() Compositor {
needsStat bool needsStat bool
compositor Compositor compositor Compositor
}{ }{
{os.Getenv("AQUEOUS_SOCKET"), true, CompositorAqueous},
{os.Getenv("MANGO_INSTANCE_SIGNATURE"), true, CompositorMango}, {os.Getenv("MANGO_INSTANCE_SIGNATURE"), true, CompositorMango},
{os.Getenv("NIRI_SOCKET"), true, CompositorNiri}, {os.Getenv("NIRI_SOCKET"), true, CompositorNiri},
{os.Getenv("SCROLLSOCK"), true, CompositorScroll}, {os.Getenv("SCROLLSOCK"), true, CompositorScroll},
@@ -78,8 +81,14 @@ func GetActiveWindow() (*WindowGeometry, error) {
return getHyprlandActiveWindow() return getHyprlandActiveWindow()
case CompositorMango: case CompositorMango:
return getMangoActiveWindow() return getMangoActiveWindow()
case CompositorAqueous:
model, err := aqueousSnapshot(context.Background())
if err != nil {
return nil, err
}
return aqueousWindowGeometry(model, "")
default: default:
return nil, fmt.Errorf("window capture requires Hyprland, Mango, or niri") return nil, fmt.Errorf("window capture requires Hyprland, Mango, niri, or Aqueous")
} }
} }
@@ -355,6 +364,9 @@ func getNiriFocusedMonitor() string {
func GetFocusedMonitor() string { func GetFocusedMonitor() string {
switch DetectCompositor() { switch DetectCompositor() {
case CompositorAqueous:
name, _ := aqueousFocusedOutput("")
return name
case CompositorHyprland: case CompositorHyprland:
return getHyprlandFocusedMonitor() return getHyprlandFocusedMonitor()
case CompositorSway: case CompositorSway:
+10
View File
@@ -220,6 +220,9 @@ func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
} }
func (s *Screenshoter) captureWindow() (*CaptureResult, error) { func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
if DetectCompositor() == CompositorAqueous {
return s.captureAqueousWindow()
}
if DetectCompositor() == CompositorNiri { if DetectCompositor() == CompositorNiri {
if s.config.Geometry { if s.config.Geometry {
return nil, fmt.Errorf("window geometry mode is not supported on niri") return nil, fmt.Errorf("window geometry mode is not supported on niri")
@@ -362,6 +365,13 @@ func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region,
} }
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) { func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
if DetectCompositor() == CompositorAqueous {
name, err := aqueousFocusedOutput(s.config.Seat)
if err != nil {
return nil, err
}
return s.captureOutput(name)
}
output := s.findFocusedOutput() output := s.findFocusedOutput()
if output == nil { if output == nil {
s.outputsMu.Lock() s.outputsMu.Lock()
+1
View File
@@ -0,0 +1 @@
aqueous-snapshot.json contains focused-window state captured from Aqueous 2d8b07fb1ff2354ea0c3acddd5cc84ceb823d6eb in an isolated session on 2026-09-05. The session token is sanitized.
@@ -0,0 +1 @@
{"schema": 1, "session": "0123456789abcdef0123456789abcdef", "sequence": "5", "base_sequence": null, "type": "snapshot", "upsert": [{"kind": "workspace", "id": "12", "output": "2", "name": "same \"name\"", "number": 3, "active": true, "urgent": false}, {"kind": "workspace", "id": "2", "output": "1", "name": "2", "number": 2, "active": false, "urgent": false}, {"kind": "session", "id": "session", "locked": false, "default_seat": "default", "overview_output": null, "overview_window": null}, {"kind": "workspace", "id": "11", "output": "2", "name": "2", "number": 2, "active": false, "urgent": false}, {"kind": "output", "id": "1", "name": "HEADLESS-2", "enabled": true, "powered": true, "bounds": {"x": 0, "y": 0, "width": 1280, "height": 720}, "usable_bounds": {"x": 0, "y": 0, "width": 1280, "height": 720}, "scale": 1, "transform": "normal", "active_workspace": "1"}, {"kind": "workspace", "id": "18", "output": "2", "name": "9", "number": 9, "active": false, "urgent": false}, {"kind": "workspace", "id": "16", "output": "2", "name": "7", "number": 7, "active": false, "urgent": false}, {"kind": "workspace", "id": "15", "output": "2", "name": "6", "number": 6, "active": false, "urgent": false}, {"kind": "workspace", "id": "10", "output": "2", "name": "1", "number": 1, "active": false, "urgent": false}, {"kind": "workspace", "id": "5", "output": "1", "name": "5", "number": 5, "active": false, "urgent": false}, {"kind": "workspace", "id": "9", "output": "1", "name": "9", "number": 9, "active": false, "urgent": false}, {"kind": "workspace", "id": "4", "output": "1", "name": "4", "number": 4, "active": false, "urgent": false}, {"kind": "seat", "id": "default", "output": "1", "window": "ca89b2eb6850b81d12b92763969d82ca", "focus_kind": "window", "keyboard": "1"}, {"kind": "workspace", "id": "3", "output": "1", "name": "3", "number": 3, "active": false, "urgent": false}, {"kind": "workspace", "id": "14", "output": "2", "name": "5", "number": 5, "active": false, "urgent": false}, {"kind": "workspace", "id": "7", "output": "1", "name": "7", "number": 7, "active": false, "urgent": false}, {"kind": "workspace", "id": "1", "output": "1", "name": "1", "number": 1, "active": true, "urgent": false}, {"kind": "workspace", "id": "6", "output": "1", "name": "6", "number": 6, "active": false, "urgent": false}, {"kind": "workspace", "id": "8", "output": "1", "name": "8", "number": 8, "active": false, "urgent": false}, {"kind": "workspace", "id": "13", "output": "2", "name": "4", "number": 4, "active": false, "urgent": false}, {"kind": "workspace", "id": "17", "output": "2", "name": "8", "number": 8, "active": false, "urgent": false}, {"kind": "output", "id": "2", "name": "HEADLESS-1", "enabled": true, "powered": true, "bounds": {"x": 1280, "y": 0, "width": 1280, "height": 720}, "usable_bounds": {"x": 1280, "y": 0, "width": 1280, "height": 720}, "scale": 1, "transform": "normal", "active_workspace": "12"}, {"kind": "window", "id": "ca89b2eb6850b81d12b92763969d82ca", "backend": "xdg", "app_id": "aq-shell-test", "class": null, "title": "same title", "workspace": "1", "output": "1", "geometry": {"x": 10, "y": 10, "width": 628, "height": 700}, "outer_geometry": {"x": 8, "y": 8, "width": 632, "height": 704}, "focused": true, "visible": true, "floating": false, "minimized": false, "maximized": false, "fullscreen": false, "skip_taskbar": false, "skip_switcher": false, "always_above": false, "always_below": false, "snapped": false, "fixed_position": false, "layout": "tiled", "can_minimize": false, "can_maximize": false, "can_activate": true}, {"kind": "keyboard", "id": "1", "seat": "default", "layouts": ["English (US)", "German"], "index": 0}, {"kind": "keyboard_device", "id": "1", "name": "wlr_virtual_keyboard_v1", "seat": "default", "group": "1", "virtual": true}], "removed": []}
+1
View File
@@ -56,6 +56,7 @@ type Output struct {
} }
type Config struct { type Config struct {
Seat string
Mode Mode Mode Mode
OutputName string OutputName string
Cursor CursorMode Cursor CursorMode
+69
View File
@@ -1,11 +1,19 @@
package utils package utils
import ( import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"time"
"unicode/utf8"
) )
type AppChecker interface { type AppChecker interface {
@@ -115,3 +123,64 @@ func userBinDirs() []string {
dirs = append(dirs, "/usr/local/bin") dirs = append(dirs, "/usr/local/bin")
return dirs return dirs
} }
const maxJSONProcessBytes = 4 * 1024 * 1024
type boundedWriter struct {
bytes.Buffer
limit int
}
func (w *boundedWriter) Write(p []byte) (int, error) {
if len(p) > w.limit-w.Len() {
return 0, errors.New("process output exceeds size bound")
}
return w.Buffer.Write(p)
}
func RunJSON(ctx context.Context, executable string, args []string, input []byte, result any) error {
if len(input) > maxJSONProcessBytes {
return errors.New("request exceeds size bound")
}
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, executable, args...)
cmd.WaitDelay = time.Second
stdout := &boundedWriter{limit: maxJSONProcessBytes + 1}
stderr := &boundedWriter{limit: 16 * 1024}
cmd.Stdout, cmd.Stderr = stdout, stderr
if input != nil {
cmd.Stdin = bytes.NewReader(input)
}
err := cmd.Run()
if ctx.Err() != nil {
return fmt.Errorf("unavailable: command completion uncertain: %w", ctx.Err())
}
if stdout.Len() != 0 {
if decodeErr := DecodeJSON(bytes.TrimSuffix(stdout.Bytes(), []byte{'\n'}), result); decodeErr != nil {
return errors.Join(err, fmt.Errorf("invalid process JSON: %w", decodeErr))
}
}
if err != nil {
return fmt.Errorf("%s: %w", executable, err)
}
if stdout.Len() == 0 {
return errors.New("missing command acknowledgement")
}
return nil
}
func DecodeJSON(data []byte, target any) error {
if len(data) > maxJSONProcessBytes || !utf8.Valid(data) {
return errors.New("invalid UTF-8 or oversized JSON record")
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(target); err != nil {
return err
}
if err := decoder.Decode(new(any)); err != io.EOF {
return errors.New("trailing JSON data")
}
return nil
}
+58
View File
@@ -1,10 +1,13 @@
package utils package utils
import ( import (
"context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -67,3 +70,58 @@ func TestEnvWithUserBinPathPrependsLocalBin(t *testing.T) {
assert.Contains(t, parts, "/usr/local/bin") assert.Contains(t, parts, "/usr/local/bin")
assert.Contains(t, env, "OTHER=value") assert.Contains(t, env, "OTHER=value")
} }
func TestJSONProcessFixture(t *testing.T) {
mode := os.Getenv("DMS_JSON_PROCESS_FIXTURE")
if mode == "" {
return
}
switch mode {
case "accepted":
fmt.Print(`{"ok":true,"status":"accepted","sequence":"9007199254740993"}`)
case "failure":
fmt.Print(`{"ok":false,"status":"locked","sequence":"1"}`)
os.Exit(1)
case "malformed":
fmt.Print("invalid JSON")
case "timeout":
time.Sleep(time.Minute)
case "oversize":
fmt.Print(strings.Repeat("x", maxJSONProcessBytes+2))
}
os.Exit(0)
}
func TestRunJSONResultsAndTimeout(t *testing.T) {
for _, mode := range []string{"accepted", "failure", "malformed", "oversize", "timeout"} {
t.Run(mode, func(t *testing.T) {
t.Setenv("DMS_JSON_PROCESS_FIXTURE", mode)
limit := 4 * time.Second
if mode == "timeout" {
limit = 100 * time.Millisecond
}
ctx, cancel := context.WithTimeout(context.Background(), limit)
defer cancel()
var result jsonProcessResult
err := RunJSON(ctx, os.Args[0], []string{"-test.run=^TestJSONProcessFixture$"}, nil, &result)
if mode == "accepted" {
if err != nil || !result.OK || result.Status != "accepted" || result.Sequence != "9007199254740993" {
t.Fatalf("lost acknowledgement: %v %#v", err, result)
}
return
}
if err == nil {
t.Fatal("failure accepted")
}
if mode == "failure" && result.Status != "locked" {
t.Fatal("operation failure lost")
}
})
}
}
type jsonProcessResult struct {
OK bool `json:"ok"`
Status string `json:"status"`
Sequence string `json:"sequence"`
}
+107
View File
@@ -0,0 +1,107 @@
function socketPath(path, runtime) {
if (!runtime.startsWith("/") || runtime.endsWith("/") || !path.startsWith(runtime + "/aqueous/"))
return false;
if (/[\r\n\u0000]/.test(path) || path.includes("//") || path.split("/").some(part => part === "." || part === ".."))
return false;
return /^\/aqueous\/[^/]+\/ipc\.sock$/.test(path.slice(runtime.length));
}
function byteLength(text) {
return encodeURIComponent(text).replace(/%[0-9A-F]{2}/g, "x").length;
}
function object(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function decimal(value) {
return typeof value === "string" && /^[0-9]{1,20}$/.test(value);
}
function nextId(value) {
const digits = value.split("");
for (let i = digits.length - 1; i >= 0; i--) {
if (digits[i] !== "9") {
digits[i] = String(Number(digits[i]) + 1);
return digits.join("");
}
digits[i] = "0";
}
if (digits.length === 20)
throw new Error("IPC request IDs exhausted");
return "1" + digits.join("");
}
function hello(value) {
if (!object(value) || value.schema !== 1 || typeof value.session !== "string" || !/^[0-9a-f]{32}$/.test(value.session) || value.max_pending_requests !== 1)
throw new Error("unsupported: Aqueous IPC hello");
const limits = {max_request_bytes: 65536, max_frame_bytes: 4259840, max_batch_bytes: 4194304};
for (const key of Object.keys(limits)) {
if (!Number.isInteger(value[key]) || value[key] <= 0 || value[key] > limits[key])
throw new Error("unsupported: IPC " + key);
}
if (!object(value.capabilities))
throw new Error("unsupported: IPC capabilities");
for (const name of ["state", "commands", "keyboard", "overview", "shortcut_inhibition"]) {
if (typeof value.capabilities[name] !== "boolean")
throw new Error("unsupported: missing capability " + name);
}
if (!value.capabilities.state)
throw new Error("unsupported: shell state");
return value;
}
function envelope(line, limit) {
if (byteLength(line) > limit)
throw new Error("IPC frame exceeds size bound");
let depth = 0, quoted = false, escaped = false;
for (const c of line) {
if (quoted) {
if (escaped) {
escaped = false;
continue;
}
switch (c) {
case "\\":
escaped = true;
break;
case '"':
quoted = false;
break;
}
continue;
}
switch (c) {
case '"':
quoted = true;
break;
case "{":
case "[":
depth++;
break;
case "}":
case "]":
depth--;
break;
}
if (depth > 32)
throw new Error("IPC nesting exceeds limit");
}
const value = JSON.parse(line);
if (!object(value) || value.ipc !== 1)
throw new Error("unsupported: IPC envelope");
return value;
}
function response(value, id) {
if (value.id !== id || !decimal(value.id) || typeof value.ok !== "boolean" || value.event !== undefined)
throw new Error("invalid IPC response correlation");
if (value.ok) {
if (!object(value.result) || value.error !== undefined)
throw new Error("invalid IPC result");
return "";
}
if (!object(value.error) || typeof value.error.code !== "string" || !value.error.code || value.error.code.length > 128 || typeof value.error.message !== "string" || byteLength(value.error.message) > 4096 || value.result !== undefined)
throw new Error("malformed IPC error");
return value.error.code + ": " + value.error.message;
}
+4
View File
@@ -1863,6 +1863,8 @@ Item {
function open(): string { function open(): string {
root.workspaceRenameModalLoader.active = true; root.workspaceRenameModalLoader.active = true;
if (root.workspaceRenameModalLoader.item) { if (root.workspaceRenameModalLoader.item) {
if (CompositorService.isAqueous)
return root.workspaceRenameModalLoader.item.show("") ? "WORKSPACE_RENAME_MODAL_OPENED" : "WORKSPACE_RENAME_UNAVAILABLE";
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId]; const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
root.workspaceRenameModalLoader.item.show(ws?.name || ""); root.workspaceRenameModalLoader.item.show(ws?.name || "");
return "WORKSPACE_RENAME_MODAL_OPENED"; return "WORKSPACE_RENAME_MODAL_OPENED";
@@ -1885,6 +1887,8 @@ Item {
root.workspaceRenameModalLoader.item.hide(); root.workspaceRenameModalLoader.item.hide();
return "WORKSPACE_RENAME_MODAL_CLOSED"; return "WORKSPACE_RENAME_MODAL_CLOSED";
} }
if (CompositorService.isAqueous)
return root.workspaceRenameModalLoader.item.show("") ? "WORKSPACE_RENAME_MODAL_OPENED" : "WORKSPACE_RENAME_UNAVAILABLE";
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId]; const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
root.workspaceRenameModalLoader.item.show(ws?.name || ""); root.workspaceRenameModalLoader.item.show(ws?.name || "");
return "WORKSPACE_RENAME_MODAL_OPENED"; return "WORKSPACE_RENAME_MODAL_OPENED";
+39 -1
View File
@@ -9,6 +9,8 @@ DankFloatingWindow {
readonly property var log: Log.scoped("WorkspaceRenameModal") readonly property var log: Log.scoped("WorkspaceRenameModal")
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property var aqueousWorkspace: null
property bool renaming: false
objectName: "workspaceRenameModal" objectName: "workspaceRenameModal"
title: I18n.tr("Rename Workspace") title: I18n.tr("Rename Workspace")
@@ -19,9 +21,24 @@ DankFloatingWindow {
onClosed: hide() onClosed: hide()
function show(name) { function show(name) {
if (CompositorService.isAqueous) {
if (renaming)
return false;
const workspace = AqueousService.workspaces.find(ws => ws.output === AqueousService.seat?.output && ws.active);
if (!AqueousService.available || !AqueousService.seat || !workspace || !AqueousService.capabilities.commands || AqueousService.locked) {
ToastService.showError(I18n.tr("Error"), I18n.tr("Unavailable"));
return false;
}
aqueousWorkspace = {
id: workspace.id,
session: workspace.aqueousSession
};
name = workspace.name;
}
nameInput.text = name; nameInput.text = name;
visible = true; visible = true;
Qt.callLater(() => nameInput.forceActiveFocus()); Qt.callLater(() => nameInput.forceActiveFocus());
return true;
} }
function hide() { function hide() {
@@ -29,6 +46,24 @@ DankFloatingWindow {
} }
function submitAndClose() { function submitAndClose() {
if (aqueousWorkspace || CompositorService.isAqueous) {
if (!aqueousWorkspace || renaming)
return;
const target = aqueousWorkspace;
renaming = true;
AqueousService.command("workspace.rename", {
id: target.id,
session: target.session,
name: nameInput.text
}, success => {
if (root.aqueousWorkspace !== target)
return;
root.renaming = false;
if (success)
root.hide();
});
return;
}
renameWorkspace(nameInput.text); renameWorkspace(nameInput.text);
hide(); hide();
} }
@@ -49,6 +84,8 @@ DankFloatingWindow {
return; return;
} }
nameInput.text = ""; nameInput.text = "";
aqueousWorkspace = null;
renaming = false;
} }
FocusScope { FocusScope {
@@ -133,7 +170,7 @@ DankFloatingWindow {
textColor: Theme.surfaceText textColor: Theme.surfaceText
placeholderText: I18n.tr("Workspace name") placeholderText: I18n.tr("Workspace name")
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.visible enabled: root.visible && !root.renaming
onAccepted: submitAndClose() onAccepted: submitAndClose()
} }
} }
@@ -190,6 +227,7 @@ DankFloatingWindow {
MouseArea { MouseArea {
id: renameArea id: renameArea
enabled: !root.renaming
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
+6 -6
View File
@@ -496,7 +496,7 @@ Item {
hasMaximizedToplevel = false; hasMaximizedToplevel = false;
return; return;
} }
if (!CompositorService.isHyprland && !CompositorService.isNiri) { if (!CompositorService.isHyprland && !CompositorService.isNiri && !CompositorService.isAqueous) {
hasMaximizedToplevel = false; hasMaximizedToplevel = false;
return; return;
} }
@@ -520,7 +520,7 @@ Item {
shouldHideForWindows = false; shouldHideForWindows = false;
return; return;
} }
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) { if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango && !CompositorService.isAqueous) {
shouldHideForWindows = false; shouldHideForWindows = false;
return; return;
} }
@@ -795,7 +795,7 @@ Item {
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
readonly property int lengthPaddingPx: Theme.px(barWindow.effectiveBarLengthPadding, barWindow._dpr) readonly property int lengthPaddingPx: Theme.px(barWindow.effectiveBarLengthPadding, barWindow._dpr)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview readonly property bool inOverviewWithShow: CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow) readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow)
@@ -986,7 +986,7 @@ Item {
} }
property bool reveal: { property bool reveal: {
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview; const inOverviewWithShow = CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow) if (inOverviewWithShow)
return true; return true;
@@ -997,7 +997,7 @@ Item {
return true; return true;
} }
if (CompositorService.isNiri && NiriService.inOverview) if (CompositorService.overviewActiveOnScreen(barWindow.screenName))
return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal; return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal;
return (barConfig?.visible ?? true) && (!autoHide || hoverReveal || popoutPinsReveal || revealSticky || ipcReveal); return (barConfig?.visible ?? true) && (!autoHide || hoverReveal || popoutPinsReveal || revealSticky || ipcReveal);
@@ -1062,7 +1062,7 @@ Item {
top: barWindow.isVertical ? parent.top : undefined top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined bottom: barWindow.isVertical ? parent.bottom : undefined
} }
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview readonly property bool inOverview: CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal
acceptedButtons: barWindow.clickThroughEnabled ? Qt.NoButton : Qt.RightButton acceptedButtons: barWindow.clickThroughEnabled ? Qt.NoButton : Qt.RightButton
enabled: !inOverview && ((barConfig?.autoHide ?? false) || !barWindow.clickThroughEnabled) enabled: !inOverview && ((barConfig?.autoHide ?? false) || !barWindow.clickThroughEnabled)
+13 -2
View File
@@ -156,6 +156,8 @@ Item {
function getRealWorkspaces() { function getRealWorkspaces() {
const screenName = _barScreenName; const screenName = _barScreenName;
if (CompositorService.isAqueous && AqueousService.available)
return AqueousService.workspacesForOutput(SettingsData.workspaceFollowFocus ? AqueousService.focusedOutput : screenName);
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
const fallbackWorkspaces = [ const fallbackWorkspaces = [
{ {
@@ -238,6 +240,8 @@ Item {
function getCurrentWorkspace() { function getCurrentWorkspace() {
const screenName = _barScreenName; const screenName = _barScreenName;
if (CompositorService.isAqueous && AqueousService.available)
return getRealWorkspaces().find(ws => ws.active)?.id || "";
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
if (!screenName || SettingsData.workspaceFollowFocus) { if (!screenName || SettingsData.workspaceFollowFocus) {
return NiriService.getCurrentWorkspaceNumber(); return NiriService.getCurrentWorkspaceNumber();
@@ -279,7 +283,14 @@ Item {
return; return;
} }
if (CompositorService.isNiri) { if (CompositorService.isAqueous && AqueousService.available) {
const index = realWorkspaces.findIndex(ws => ws.id === getCurrentWorkspace());
if (index < 0)
return;
const next = Math.max(0, Math.min(realWorkspaces.length - 1, index + (direction > 0 ? 1 : -1)));
if (next !== index)
AqueousService.activateWorkspace(realWorkspaces[next]);
} else if (CompositorService.isNiri) {
const currentWs = getCurrentWorkspace(); const currentWs = getCurrentWorkspace();
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs); const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs);
const validIndex = currentIndex === -1 ? 0 : currentIndex; const validIndex = currentIndex === -1 ? 0 : currentIndex;
@@ -323,7 +334,7 @@ Item {
} }
function switchApp(deltaY) { function switchApp(deltaY) {
const windows = sortedToplevels; const windows = sortedToplevels.filter(w => !w.skipSwitcher);
if (windows.length < 2) { if (windows.length < 2) {
return; return;
} }
+1 -1
View File
@@ -67,7 +67,7 @@ PanelWindow {
implicitWidth: body.surfaceImplicitWidth implicitWidth: body.surfaceImplicitWidth
exclusiveZone: body.surfaceExclusiveZone exclusiveZone: body.surfaceExclusiveZone
BackgroundEffect.blurRegion: body.blurRegion BackgroundEffect.blurRegion: BlurService.enabled ? body.blurRegion : null
Component.onCompleted: { Component.onCompleted: {
KeyboardFocus.registerBarWindow(barWindow); KeyboardFocus.registerBarWindow(barWindow);
@@ -160,7 +160,7 @@ PanelWindow {
return []; return [];
const toplevels = []; const toplevels = [];
const allToplevels = ToplevelManager.toplevels.values; const allToplevels = CompositorService.isAqueous && AqueousService.available ? CompositorService.sortedToplevels : ToplevelManager.toplevels.values;
for (let i = 0; i < allToplevels.length; i++) { for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]; const toplevel = allToplevels[i];
if (toplevel.appId === root.appData.appId) { if (toplevel.appId === root.appData.appId) {
@@ -31,7 +31,7 @@ BasePill {
property int availableWidth: maxWidth property int availableWidth: maxWidth
readonly property real effectiveHorizontalWidth: Math.max(0, Math.min(maxWidth, availableWidth)) readonly property real effectiveHorizontalWidth: Math.max(0, Math.min(maxWidth, availableWidth))
readonly property real effectiveHorizontalInnerWidth: Math.max(0, effectiveHorizontalWidth - horizontalPadding * 2) readonly property real effectiveHorizontalInnerWidth: Math.max(0, effectiveHorizontalWidth - horizontalPadding * 2)
property Toplevel activeWindow: null property var activeWindow: null
property var activeDesktopEntry: null property var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse property bool isHovered: mouseArea.containsMouse
property bool isAutoHideBar: false property bool isAutoHideBar: false
@@ -99,6 +99,11 @@ BasePill {
} }
function updateActiveWindow() { function updateActiveWindow() {
if (CompositorService.isAqueous && AqueousService.available) {
const focused = AqueousService.focusedWindow;
activeWindow = focused && (!parentScreen || focused.screens.some(s => s.name === parentScreen.name)) ? focused : null;
return;
}
let active = ToplevelManager.activeToplevel; let active = ToplevelManager.activeToplevel;
if (!active && CompositorService.isNiri) { if (!active && CompositorService.isNiri) {
@@ -16,6 +16,8 @@ BasePill {
property bool showIcon: widgetData?.keyboardLayoutNameShowIcon !== undefined ? widgetData.keyboardLayoutNameShowIcon : SettingsData.keyboardLayoutNameShowIcon property bool showIcon: widgetData?.keyboardLayoutNameShowIcon !== undefined ? widgetData.keyboardLayoutNameShowIcon : SettingsData.keyboardLayoutNameShowIcon
readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"] readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"]
property string currentLayout: { property string currentLayout: {
if (CompositorService.isAqueous)
return AqueousService.keyboardLayout;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
return NiriService.getCurrentKeyboardLayoutName(); return NiriService.getCurrentKeyboardLayoutName();
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
@@ -25,7 +27,7 @@ BasePill {
} }
property string hyprlandKeyboard: "" property string hyprlandKeyboard: ""
property var hyprlandLayoutLabels: [] property var hyprlandLayoutLabels: []
readonly property var _allLayoutLabels: CompositorService.isNiri ? (NiriService.keyboardLayoutNames || []).map(n => displayLabel(n)) : hyprlandLayoutLabels readonly property var _allLayoutLabels: CompositorService.isAqueous ? AqueousService.keyboardLayouts.map(n => displayLabel(n)) : CompositorService.isNiri ? (NiriService.keyboardLayoutNames || []).map(n => displayLabel(n)) : hyprlandLayoutLabels
readonly property string reserveLabel: widestLabel(_allLayoutLabels) readonly property string reserveLabel: widestLabel(_allLayoutLabels)
readonly property string verticalReserveLabel: widestLabel(_allLayoutLabels.map(n => LayoutCodes.layoutCode(n))) readonly property string verticalReserveLabel: widestLabel(_allLayoutLabels.map(n => LayoutCodes.layoutCode(n)))
@@ -131,6 +133,8 @@ BasePill {
onClicked: { onClicked: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout(); NiriService.cycleKeyboardLayout();
} else if (CompositorService.isAqueous) {
AqueousService.cycleKeyboardLayout();
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]); Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
@@ -163,7 +163,7 @@ BasePill {
const deltaY = wheelEvent.angleDelta.y; const deltaY = wheelEvent.angleDelta.y;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const windows = root.sortedToplevels; const windows = root.sortedToplevels.filter(w => !w.skipSwitcher);
if (windows.length < 2) if (windows.length < 2)
return; return;
@@ -236,7 +236,7 @@ BasePill {
id: windowRepeater id: windowRepeater
model: ScriptModel { model: ScriptModel {
values: _groupByApp ? groupedWindows : sortedToplevels values: _groupByApp ? groupedWindows : sortedToplevels
objectProp: _groupByApp ? "appId" : "address" objectProp: _groupByApp ? "appId" : (CompositorService.isAqueous && AqueousService.available ? "aqueousKey" : "address")
} }
delegate: Item { delegate: Item {
@@ -494,7 +494,7 @@ BasePill {
id: windowRepeater id: windowRepeater
model: ScriptModel { model: ScriptModel {
values: _groupByApp ? groupedWindows : sortedToplevels values: _groupByApp ? groupedWindows : sortedToplevels
objectProp: _groupByApp ? "appId" : "address" objectProp: _groupByApp ? "appId" : (CompositorService.isAqueous && AqueousService.available ? "aqueousKey" : "address")
} }
delegate: Item { delegate: Item {
@@ -24,6 +24,7 @@ Item {
property real crossEdgeExtension: 0 property real crossEdgeExtension: 0
readonly property bool isMango: CompositorService.isMango readonly property bool isMango: CompositorService.isMango
readonly property bool useAqueous: CompositorService.isAqueous && AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1"
readonly property real _leftMargin: { readonly property real _leftMargin: {
if (isVertical) if (isVertical)
@@ -82,8 +83,10 @@ Item {
} }
readonly property bool useUnfocusedAppearance: !isFocusedMonitor && SettingsData.workspaceUnfocusedMonitorSeparateAppearance && BarWidgetService.focusedScreenDetectionSupported readonly property bool useUnfocusedAppearance: !isFocusedMonitor && SettingsData.workspaceUnfocusedMonitorSeparateAppearance && BarWidgetService.focusedScreenDetectionSupported
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(CompositorService.isAqueous ? Quickshell.screens.find(s => s.name === effectiveScreenName) || parentScreen : parentScreen) : null
readonly property bool useExtWorkspace: { readonly property bool useExtWorkspace: {
if (useAqueous)
return false;
if (Quickshell.env("DMS_FORCE_EXTWS") === "1") if (Quickshell.env("DMS_FORCE_EXTWS") === "1")
return (WindowManager.windowsets?.length ?? 0) > 0; return (WindowManager.windowsets?.length ?? 0) > 0;
if (!CompositorService.compositorDetected) if (!CompositorService.compositorDetected)
@@ -117,6 +120,8 @@ Item {
} }
property var currentWorkspace: { property var currentWorkspace: {
if (useAqueous)
return AqueousService.workspacesForOutput(effectiveScreenName).find(w => w.active)?.id || "";
if (useExtWorkspace) if (useExtWorkspace)
return getExtWorkspaceActiveWorkspace(); return getExtWorkspaceActiveWorkspace();
@@ -143,6 +148,10 @@ Item {
return []; return [];
} }
property var workspaceList: { property var workspaceList: {
if (useAqueous) {
const baseList = AqueousService.workspacesForOutput(effectiveScreenName).filter(ws => !SettingsData.showOccupiedWorkspacesOnly || ws.active || AqueousService.toplevels.some(w => w.aqueousWorkspaceId === ws.id));
return SettingsData.showWorkspacePadding && baseList.length ? padWorkspaces(baseList) : baseList;
}
if (useExtWorkspace) { if (useExtWorkspace) {
const baseList = getExtWorkspaceWorkspaces(); const baseList = getExtWorkspaceWorkspaces();
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList; return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
@@ -322,12 +331,14 @@ Item {
function getWorkspaceIcons(ws) { function getWorkspaceIcons(ws) {
_desktopEntriesUpdateTrigger; _desktopEntriesUpdateTrigger;
if (!SettingsData.showWorkspaceApps || !ws) { if (!SettingsData.showWorkspaceApps || !ws || useExtWorkspace || (useAqueous && ws._placeholder)) {
return []; return [];
} }
let targetWorkspaceId; let targetWorkspaceId;
if (CompositorService.isNiri) { if (useAqueous) {
targetWorkspaceId = ws.id;
} else if (CompositorService.isNiri) {
if (!ws || typeof ws !== "object") { if (!ws || typeof ws !== "object") {
const wsNumber = typeof ws === "number" ? ws : -1; const wsNumber = typeof ws === "number" ? ws : -1;
if (wsNumber <= 0) { if (wsNumber <= 0) {
@@ -387,7 +398,9 @@ Item {
return; return;
} else { } else {
let winWs = null; let winWs = null;
if (CompositorService.isNiri) { if (useAqueous) {
winWs = w.aqueousWorkspaceId;
} else if (CompositorService.isNiri) {
winWs = w.workspace_id; winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num; winWs = w.workspace?.num;
@@ -405,7 +418,7 @@ Item {
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown"); const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
const moddedId = Paths.moddedAppId(keyBase); const moddedId = Paths.moddedAppId(keyBase);
const groupThisWs = SettingsData.groupWorkspaceApps && (!isActiveWs || SettingsData.groupActiveWorkspaceApps); const groupThisWs = SettingsData.groupWorkspaceApps && (!isActiveWs || SettingsData.groupActiveWorkspaceApps);
const key = groupThisWs ? moddedId : `${moddedId}_${i}`; const key = groupThisWs ? moddedId : (w.aqueousKey || `${moddedId}_${i}`);
if (!byApp[key]) { if (!byApp[key]) {
const isQuickshell = keyBase === "org.quickshell" || keyBase === "com.danklinux.dms"; const isQuickshell = keyBase === "org.quickshell" || keyBase === "com.danklinux.dms";
@@ -421,6 +434,7 @@ Item {
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)), "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
"count": 1, "count": 1,
"windowId": w.address || w.id, "windowId": w.address || w.id,
"windowSession": useAqueous ? w.aqueousSession : "",
"fallbackText": appName || "" "fallbackText": appName || ""
}; };
} else { } else {
@@ -435,7 +449,7 @@ Item {
} }
function _makePlaceholder() { function _makePlaceholder() {
if (useExtWorkspace) if (useAqueous || useExtWorkspace)
return { return {
"id": "", "id": "",
"name": "", "name": "",
@@ -648,7 +662,7 @@ Item {
if (!extProjection) if (!extProjection)
return ""; return "";
const activeWs = extProjection.windowsets.find(ws => ws.active); const activeWs = extProjection.windowsets.find(ws => ws.active);
return activeWs ? (activeWs.id || activeWs.name || "") : ""; return activeWs || null;
} }
readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1 readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1
@@ -659,7 +673,7 @@ Item {
function getRealWorkspaces() { function getRealWorkspaces() {
return root.workspaceList.filter(ws => { return root.workspaceList.filter(ws => {
if (useExtWorkspace) if (useAqueous || useExtWorkspace)
return ws && !ws._placeholder; return ws && !ws._placeholder;
if (CompositorService.isNiri) if (CompositorService.isNiri)
return ws && ws.idx !== -1; return ws && ws.idx !== -1;
@@ -674,9 +688,14 @@ Item {
} }
function switchToWorkspaceByModelData(data) { function switchToWorkspaceByModelData(data) {
if (!data) if (!data || data._placeholder)
return; return;
if (useAqueous) {
AqueousService.activateWorkspace(data);
return;
}
if (root.useExtWorkspace) { if (root.useExtWorkspace) {
if (typeof data.activate === "function") if (typeof data.activate === "function")
data.activate(); data.activate();
@@ -727,13 +746,21 @@ Item {
} }
function switchWorkspace(direction) { function switchWorkspace(direction) {
if (useAqueous) {
const workspaces = getRealWorkspaces();
const index = workspaces.findIndex(w => w.id === currentWorkspace);
const next = Math.max(0, Math.min(workspaces.length - 1, index + (direction > 0 ? 1 : -1)));
if (next !== index)
AqueousService.activateWorkspace(workspaces[next]);
return;
}
if (useExtWorkspace) { if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) { if (realWorkspaces.length < 2) {
return; return;
} }
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace); const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace);
const validIndex = currentIndex === -1 ? 0 : currentIndex; const validIndex = currentIndex === -1 ? 0 : currentIndex;
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
@@ -812,6 +839,8 @@ Item {
} }
function getWorkspaceIndexFallback(modelData, index) { function getWorkspaceIndexFallback(modelData, index) {
if (useAqueous)
return modelData?.number ?? "";
if (root.useExtWorkspace) if (root.useExtWorkspace)
return index + 1; return index + 1;
if (CompositorService.isNiri) if (CompositorService.isNiri)
@@ -827,8 +856,8 @@ Item {
function getWorkspaceIndex(modelData, index) { function getWorkspaceIndex(modelData, index) {
let isPlaceholder; let isPlaceholder;
if (root.useExtWorkspace) { if (root.useAqueous || root.useExtWorkspace) {
isPlaceholder = modelData?.hidden === true; isPlaceholder = modelData?._placeholder === true;
} else if (CompositorService.isNiri) { } else if (CompositorService.isNiri) {
isPlaceholder = modelData?.idx === -1; isPlaceholder = modelData?.idx === -1;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
@@ -868,7 +897,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index); return getWorkspaceIndexFallback(modelData, index);
} }
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle readonly property bool hasNativeWorkspaceSupport: useAqueous || CompositorService.isNiri || CompositorService.isHyprland || root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0 readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -962,7 +991,9 @@ Item {
const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y); const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
switch (mouse.button) { switch (mouse.button) {
case Qt.RightButton: case Qt.RightButton:
if (CompositorService.isNiri) { if (CompositorService.isAqueous) {
AqueousService.toggleOverview(root.effectiveScreenName);
} else if (CompositorService.isNiri) {
NiriService.toggleOverview(); NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
@@ -1144,8 +1175,10 @@ Item {
} }
property bool isActive: { property bool isActive: {
if (root.useAqueous)
return modelData?.active === true;
if (root.useExtWorkspace) if (root.useExtWorkspace)
return (modelData?.id || modelData?.name) === root.currentWorkspace; return modelData === root.currentWorkspace;
if (CompositorService.isNiri) if (CompositorService.isNiri)
return !!(modelData && modelData.idx === root.currentWorkspace); return !!(modelData && modelData.idx === root.currentWorkspace);
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
@@ -1157,6 +1190,8 @@ Item {
return modelData === root.currentWorkspace; return modelData === root.currentWorkspace;
} }
property bool isOccupied: { property bool isOccupied: {
if (root.useAqueous)
return AqueousService.toplevels.some(w => w.aqueousWorkspaceId === modelData?.id);
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id); return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
if (root.isMango) if (root.isMango)
@@ -1166,7 +1201,7 @@ Item {
return false; return false;
} }
property bool isPlaceholder: { property bool isPlaceholder: {
if (root.useExtWorkspace) if (root.useAqueous || root.useExtWorkspace)
return !!(modelData && modelData._placeholder); return !!(modelData && modelData._placeholder);
if (CompositorService.isNiri) if (CompositorService.isNiri)
return !!(modelData && modelData.idx === -1); return !!(modelData && modelData.idx === -1);
@@ -1183,6 +1218,8 @@ Item {
property var loadedWorkspaceData: null property var loadedWorkspaceData: null
property bool loadedIsUrgent: false property bool loadedIsUrgent: false
property bool isUrgent: { property bool isUrgent: {
if (root.useAqueous)
return modelData?.urgent === true;
if (root.useExtWorkspace) if (root.useExtWorkspace)
return modelData?.urgent ?? false; return modelData?.urgent ?? false;
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
@@ -1210,6 +1247,9 @@ Item {
if (!SettingsData.showWorkspaceApps || isPlaceholder) if (!SettingsData.showWorkspaceApps || isPlaceholder)
return 0; return 0;
if (root.useAqueous)
return loadedIcons.length;
let targetWorkspaceId; let targetWorkspaceId;
if (root.useExtWorkspace) { if (root.useExtWorkspace) {
targetWorkspaceId = modelData?.id || modelData?.name; targetWorkspaceId = modelData?.id || modelData?.name;
@@ -1267,7 +1307,7 @@ Item {
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2))
readonly property real baseHeight: root.isVertical ? (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) readonly property real baseHeight: root.isVertical ? (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== "" readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (root.useAqueous || CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0 readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0
readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0 readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0
@@ -1481,7 +1521,9 @@ Item {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
if (delegateRoot.focusWindowAt(mouse.x, mouse.y)) if (delegateRoot.focusWindowAt(mouse.x, mouse.y))
return; return;
if (root.useExtWorkspace) { if (root.useAqueous) {
root.switchToWorkspaceByModelData(modelData);
} else if (root.useExtWorkspace) {
if (typeof modelData?.activate === "function") if (typeof modelData?.activate === "function")
modelData.activate(); modelData.activate();
} else if (CompositorService.isNiri) { } else if (CompositorService.isNiri) {
@@ -1496,7 +1538,9 @@ Item {
CompositorService.dispatchSwayWorkspace(modelData); CompositorService.dispatchSwayWorkspace(modelData);
} }
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) { if (CompositorService.isAqueous) {
AqueousService.toggleOverview(root.effectiveScreenName);
} else if (CompositorService.isNiri) {
NiriService.toggleOverview(); NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
@@ -1519,7 +1563,7 @@ Item {
} }
var wsData = null; var wsData = null;
if (root.useExtWorkspace) { if (root.useAqueous || root.useExtWorkspace) {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isNiri) { } else if (CompositorService.isNiri) {
wsData = modelData || null; wsData = modelData || null;
@@ -1561,13 +1605,20 @@ Item {
if (!layout) if (!layout)
return null; return null;
const point = layout.mapFromItem(mouseArea, x, y); const point = layout.mapFromItem(mouseArea, x, y);
return layout.childAt(point.x, point.y)?.windowId ?? null; const icon = layout.childAt(point.x, point.y);
if (root.useAqueous)
return icon?.windowId ? {id: icon.windowId, session: icon.windowSession} : null;
return icon?.windowId ?? null;
} }
function focusWindowAt(x, y) { function focusWindowAt(x, y) {
const winId = delegateRoot.windowIdAt(x, y); const winId = delegateRoot.windowIdAt(x, y);
if (!winId) if (!winId)
return false; return false;
if (root.useAqueous) {
AqueousService.command("window.activate", winId);
return true;
}
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
HyprlandService.focusWindow(winId); HyprlandService.focusWindow(winId);
return true; return true;
@@ -1770,6 +1821,7 @@ Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property var windowId: modelData.windowId readonly property var windowId: modelData.windowId
readonly property string windowSession: modelData.windowSession || ""
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
@@ -1927,6 +1979,7 @@ Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property var windowId: modelData.windowId readonly property var windowId: modelData.windowId
readonly property string windowSession: modelData.windowSession || ""
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
@@ -2123,6 +2176,13 @@ Item {
delegateRoot.updateAllData(); delegateRoot.updateAllData();
} }
} }
Connections {
target: AqueousService
function onStateChanged() {
if (root.useAqueous)
delegateRoot.updateAllData();
}
}
property var _extWindowsetsTrigger: root.useExtWorkspace ? WindowManager.windowsets : null property var _extWindowsetsTrigger: root.useExtWorkspace ? WindowManager.windowsets : null
on_ExtWindowsetsTriggerChanged: { on_ExtWindowsetsTriggerChanged: {
if (root.useExtWorkspace) if (root.useExtWorkspace)
+1 -1
View File
@@ -96,7 +96,7 @@ Item {
return toplevel.activated; return toplevel.activated;
} else if (appData.type === "grouped") { } else if (appData.type === "grouped") {
// For grouped apps, check if any window is focused // For grouped apps, check if any window is focused
const allToplevels = ToplevelManager.toplevels.values; const allToplevels = CompositorService.isAqueous && AqueousService.available ? CompositorService.sortedToplevels : ToplevelManager.toplevels.values;
for (let i = 0; i < allToplevels.length; i++) { for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]; const toplevel = allToplevels[i];
if (toplevel.appId === appData.appId && toplevel.activated) { if (toplevel.appId === appData.appId && toplevel.activated) {
+1 -1
View File
@@ -262,7 +262,7 @@ Item {
const separatePinnedAndRunning = SettingsData.dockSeparatePinnedAndRunningApps; const separatePinnedAndRunning = SettingsData.dockSeparatePinnedAndRunningApps;
sortedToplevels.forEach((toplevel, index) => { sortedToplevels.forEach((toplevel, index) => {
let uniqueKey = "window_" + index; let uniqueKey = toplevel.aqueousKey || "window_" + index;
if (CompositorService.isHyprland && Hyprland.toplevels) { if (CompositorService.isHyprland && Hyprland.toplevels) {
const hyprlandToplevels = Array.from(Hyprland.toplevels.values); const hyprlandToplevels = Array.from(Hyprland.toplevels.values);
for (let i = 0; i < hyprlandToplevels.length; i++) { for (let i = 0; i < hyprlandToplevels.length; i++) {
+5 -2
View File
@@ -249,7 +249,7 @@ Item {
readonly property bool shouldHideForWindows: { readonly property bool shouldHideForWindows: {
if (!SettingsData.dockSmartAutoHide) if (!SettingsData.dockSmartAutoHide)
return false; return false;
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango && !CompositorService.isAqueous)
return false; return false;
const screenName = dock.modelData?.name ?? ""; const screenName = dock.modelData?.name ?? "";
@@ -313,6 +313,9 @@ Item {
return false; return false;
} }
if (CompositorService.isAqueous && AqueousService.available)
return AqueousService.overlapsDock(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
if (CompositorService.isMango) { if (CompositorService.isMango) {
MangoService.windows; MangoService.windows;
MangoService.outputs; MangoService.outputs;
@@ -348,7 +351,7 @@ Item {
onTriggered: dock.startupRevealDone = true onTriggered: dock.startupRevealDone = true
} }
readonly property bool overviewReveal: CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview readonly property bool overviewReveal: CompositorService.overviewActiveOnScreen(screenName) && SettingsData.dockOpenOnOverview
readonly property bool hoverOrActive: dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky readonly property bool hoverOrActive: dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
onOverviewRevealChanged: { onOverviewRevealChanged: {
+1 -1
View File
@@ -87,7 +87,7 @@ PanelWindow {
readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen) readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
readonly property bool _dockHostedHere: { readonly property bool _dockHostedHere: {
const dockVisible = SettingsData.showDock || (CompositorService.isNiri && SettingsData.dockOpenOnOverview); const dockVisible = SettingsData.showDock || ((CompositorService.isNiri || CompositorService.isAqueous) && SettingsData.dockOpenOnOverview);
if (!win._connectedActive || !dockVisible || SettingsData.dockUseOverlayLayer) if (!win._connectedActive || !dockVisible || SettingsData.dockUseOverlayLayer)
return false; return false;
const screens = SettingsData.getFilteredScreens("dock"); const screens = SettingsData.getFilteredScreens("dock");
+15 -2
View File
@@ -18,6 +18,7 @@ Item {
property bool isMiracle: CompositorService.isMiracle property bool isMiracle: CompositorService.isMiracle
property bool isMango: CompositorService.isMango property bool isMango: CompositorService.isMango
property bool isLabwc: CompositorService.isLabwc property bool isLabwc: CompositorService.isLabwc
property bool isAqueous: CompositorService.isAqueous
property string compositorName: { property string compositorName: {
if (isHyprland) if (isHyprland)
@@ -32,6 +33,8 @@ Item {
return "mangowc"; return "mangowc";
if (isLabwc) if (isLabwc)
return "labwc"; return "labwc";
if (isAqueous)
return "aqueous";
return "niri"; return "niri";
} }
@@ -48,6 +51,8 @@ Item {
return "/assets/mango.png"; return "/assets/mango.png";
if (isLabwc) if (isLabwc)
return "/assets/labwc.png"; return "/assets/labwc.png";
if (isAqueous)
return "/assets/aqueous.svg";
return "/assets/niri.svg"; return "/assets/niri.svg";
} }
@@ -64,6 +69,8 @@ Item {
return "https://github.com/DreamMaoMao/mangowc"; return "https://github.com/DreamMaoMao/mangowc";
if (isLabwc) if (isLabwc)
return "https://labwc.github.io/"; return "https://labwc.github.io/";
if (isAqueous)
return "";
return "https://github.com/niri-wm/niri"; return "https://github.com/niri-wm/niri";
} }
@@ -80,6 +87,8 @@ Item {
return I18n.tr("mangowc GitHub"); return I18n.tr("mangowc GitHub");
if (isLabwc) if (isLabwc)
return I18n.tr("LabWC Website"); return I18n.tr("LabWC Website");
if (isAqueous)
return "Aqueous";
return I18n.tr("niri GitHub"); return I18n.tr("niri GitHub");
} }
@@ -376,11 +385,15 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: compositorUrl === "" ? Qt.ArrowCursor : Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
onEntered: parent.hovered = true onEntered: parent.hovered = true
onExited: parent.hovered = false onExited: parent.hovered = false
onClicked: Qt.openUrlExternally(compositorUrl) onClicked: {
if (compositorUrl === "")
return;
Qt.openUrlExternally(compositorUrl);
}
} }
} }
+3 -3
View File
@@ -874,15 +874,15 @@ Item {
height: 1 height: 1
color: Theme.outline color: Theme.outline
opacity: 0.15 opacity: 0.15
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop visible: (CompositorService.isNiri || CompositorService.isAqueous) && !dankBarTab.islandOwnsSelectedBarTop
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barOpenOnOverview" settingKey: "barOpenOnOverview"
tags: ["bar", "overview", "niri", "show", "frame"] tags: ["bar", "overview", "niri", "show", "frame"]
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop visible: (CompositorService.isNiri || CompositorService.isAqueous) && !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Show on Overview") text: I18n.tr("Show on Overview")
description: dankBarTab.selectedBarFrameStyled ? I18n.tr("Show during Niri overview") : I18n.tr("Show the bar when niri overview is active") description: CompositorService.isAqueous ? I18n.tr("Show on Overview") : dankBarTab.selectedBarFrameStyled ? I18n.tr("Show during Niri overview") : I18n.tr("Show the bar when niri overview is active")
checked: dankBarTab.selectedBarFrameStyled ? SettingsData.frameShowOnOverview : (selectedBarConfig?.openOnOverview ?? false) checked: dankBarTab.selectedBarFrameStyled ? SettingsData.frameShowOnOverview : (selectedBarConfig?.openOnOverview ?? false)
onToggled: toggled => { onToggled: toggled => {
if (dankBarTab.selectedBarFrameStyled) { if (dankBarTab.selectedBarFrameStyled) {
@@ -41,6 +41,197 @@ Singleton {
property bool hasPendingChanges: Object.keys(pendingChanges).length > 0 || Object.keys(pendingNiriChanges).length > 0 || Object.keys(pendingHyprlandChanges).length > 0 || formatChanged property bool hasPendingChanges: Object.keys(pendingChanges).length > 0 || Object.keys(pendingNiriChanges).length > 0 || Object.keys(pendingHyprlandChanges).length > 0 || formatChanged
property bool validatingConfig: false property bool validatingConfig: false
property var _cancelOutputWrite: null
property var aqueousPreview: null
function outputFingerprint(outputs) {
return JSON.stringify(outputs.map(o => ({
name: o.name,
id: o.id,
enabled: o.enabled,
x: o.x,
y: o.y,
scale: o.scale,
transform: o.transform,
mode: o.currentMode ? [o.currentMode.width, o.currentMode.height, o.currentMode.refresh] : null,
adaptiveSync: o.adaptiveSync
})).sort((a, b) => a.name.localeCompare(b.name)));
}
function outputHeads(outputs) {
return outputs.map(o => ({
name: o.name,
enabled: o.enabled,
modeId: o.currentMode?.id,
position: {
x: o.x,
y: o.y
},
scale: o.scale,
transform: o.transform,
adaptiveSync: o.adaptiveSync
}));
}
function outputHeadsMatch(candidate, actual, original) {
if (candidate.length !== actual.length || original.length !== actual.length || original.some(o => !actual.some(a => a.id === o.id && a.name === o.name)))
return false;
return candidate.every(h => {
const output = actual.find(o => o.name === h.name);
if (!output || output.enabled !== h.enabled)
return false;
if (!h.enabled)
return true;
const mode = h.customMode || original.find(o => o.name === h.name)?.modes?.find(m => m.id === h.modeId);
return output.x === h.position.x && output.y === h.position.y && Math.abs(output.scale - h.scale) < 0.0001 && output.transform === h.transform && (h.adaptiveSync === undefined || output.adaptiveSync === h.adaptiveSync) && !!mode && output.currentMode?.width === mode.width && output.currentMode?.height === mode.height && output.currentMode?.refresh === mode.refresh;
});
}
function freshAqueousOutputs(callback) {
DMSService.sendRequest("wlroutput.getState", null, response => {
callback(response.result?.outputs || null, response.error || "");
}, 5000);
}
function aqueousDisplayError(message) {
validatingConfig = false;
validationError = AqueousService.errorMessage(message);
ToastService.showError(I18n.tr("Error"), validationError, message);
}
function discardAqueousPreview() {
if (validatingConfig)
return;
aqueousPreview = null;
restoreDisplayNameMode();
validationError = "";
clearPendingChanges();
WlrOutputService.requestState();
}
function previewAqueousOutputs(descriptions) {
if (validatingConfig)
return;
if (!AqueousService.available) {
aqueousDisplayError("unavailable: compositor state");
return;
}
if (aqueousPreview) {
observeAqueousPreview(aqueousPreview, descriptions, "");
return;
}
const session = AqueousService.session;
validatingConfig = true;
AqueousConfigService.load((snapshot, error) => {
if (!snapshot) {
aqueousDisplayError(error);
return;
}
freshAqueousOutputs((original, error) => {
if (AqueousService.session !== session) {
aqueousDisplayError("conflict: compositor session changed");
return;
}
if (!original) {
aqueousDisplayError(error);
return;
}
const candidate = WlrOutputService.outputsConfigHeads(buildOutputsWithPendingChanges(), outputs);
WlrOutputService.testConfiguration(candidate, (success, message) => {
if (AqueousService.session !== session) {
aqueousDisplayError("conflict: compositor session changed");
return;
}
if (!success) {
aqueousDisplayError(message);
return;
}
const preview = {
snapshot: snapshot,
original: original,
heads: candidate,
fingerprint: null,
session: session
};
aqueousPreview = preview;
WlrOutputService.applyConfiguration(candidate, (success, message) => {
observeAqueousPreview(preview, descriptions, success ? "" : message);
});
});
});
});
}
function observeAqueousPreview(preview, descriptions, applyError) {
validatingConfig = true;
freshAqueousOutputs((actual, error) => {
if (aqueousPreview !== preview)
return;
if (!actual || AqueousService.session !== preview.session || !outputHeadsMatch(preview.heads, actual, preview.original)) {
if (actual && outputFingerprint(actual) === outputFingerprint(preview.original))
aqueousPreview = null;
aqueousDisplayError(error || applyError || "conflict: display preview changed externally");
return;
}
preview.fingerprint = outputFingerprint(actual);
validatingConfig = false;
validationError = applyError ? AqueousService.errorMessage(applyError) : "";
changesApplied(descriptions);
});
}
function finishAqueousPreview(keep) {
if (validatingConfig || !aqueousPreview)
return;
const preview = aqueousPreview;
validatingConfig = true;
freshAqueousOutputs((actual, error) => {
if (!actual || AqueousService.session !== preview.session || outputFingerprint(actual) !== preview.fingerprint) {
aqueousDisplayError(error || "conflict: output configuration changed after preview");
return;
}
if (keep) {
let draft;
try {
draft = AqueousConfigService.buildOutputsConfig(preview.snapshot, actual, preview.original);
} catch (e) {
aqueousDisplayError(String(e));
return;
}
AqueousConfigService.apply(draft, (snapshot, message) => {
if (!snapshot) {
aqueousDisplayError(message);
return;
}
aqueousPreview = null;
if (formatChanged)
SettingsData.saveSettings();
clearPendingChanges();
freshAqueousOutputs((live, error) => {
validatingConfig = false;
if (!live || AqueousService.session !== preview.session || outputFingerprint(live) !== preview.fingerprint) {
aqueousDisplayError("conflict: configuration saved but live display state changed");
return;
}
changesConfirmed();
});
});
return;
}
WlrOutputService.applyConfiguration(outputHeads(preview.original), (success, message) => {
if (!success) {
aqueousDisplayError(message);
return;
}
validatingConfig = false;
aqueousPreview = null;
restoreDisplayNameMode();
clearPendingChanges();
WlrOutputService.requestState();
changesReverted();
});
});
}
property string validationError: "" property string validationError: ""
property var currentOutputSet: [] property var currentOutputSet: []
@@ -1707,9 +1898,21 @@ Singleton {
MangoService.generateOutputsConfig(outputsData, finish); MangoService.generateOutputsConfig(outputsData, finish);
break; break;
default: default:
WlrOutputService.applyOutputsConfig(outputsData, outputs); {
finish(true); if (_cancelOutputWrite)
break; _cancelOutputWrite();
let completed = false;
const complete = success => {
if (completed)
return;
completed = true;
root._cancelOutputWrite = null;
finish(success);
};
_cancelOutputWrite = () => complete(false);
WlrOutputService.applyOutputsConfig(outputsData, outputs, complete);
break;
}
} }
return true; return true;
} }
@@ -2145,11 +2348,15 @@ Singleton {
originalDisplayNameMode = ""; originalDisplayNameMode = "";
} }
function restoreDisplayNameMode() {
if (originalDisplayNameMode === "")
return;
SettingsData.displayNameMode = originalDisplayNameMode;
SettingsData.saveSettings();
}
function discardChanges() { function discardChanges() {
if (originalDisplayNameMode !== "") { restoreDisplayNameMode();
SettingsData.displayNameMode = originalDisplayNameMode;
SettingsData.saveSettings();
}
backendFetchOutputs(); backendFetchOutputs();
clearPendingChanges(); clearPendingChanges();
} }
@@ -2221,16 +2428,32 @@ Singleton {
return; return;
} }
changesApplied(changeDescriptions); if (CompositorService.isAqueous) {
previewAqueousOutputs(changeDescriptions);
if (formatChanged) return;
SettingsData.saveSettings(); }
if (CompositorService.isHyprland)
commitHyprlandSettingsChanges();
const mergedOutputs = buildOutputsWithPendingChanges(); const mergedOutputs = buildOutputsWithPendingChanges();
backendWriteOutputsConfig(mergedOutputs); if (CompositorService.isHyprland || CompositorService.isMango) {
changesApplied(changeDescriptions);
if (formatChanged)
SettingsData.saveSettings();
if (CompositorService.isHyprland)
commitHyprlandSettingsChanges();
backendWriteOutputsConfig(mergedOutputs);
return;
}
validatingConfig = true;
backendWriteOutputsConfig(mergedOutputs, success => {
validatingConfig = false;
if (!success) {
ToastService.showError(I18n.tr("Error"), I18n.tr("Failed to apply profile"));
return;
}
if (formatChanged)
SettingsData.saveSettings();
changesApplied(changeDescriptions);
});
} }
function validateAndApplyNiriConfig(changeDescriptions) { function validateAndApplyNiriConfig(changeDescriptions) {
@@ -2341,6 +2564,10 @@ Singleton {
} }
function confirmChanges(profileId) { function confirmChanges(profileId) {
if (CompositorService.isAqueous) {
finishAqueousPreview(true);
return;
}
const outputConfigs = buildCurrentOutputConfigs(); const outputConfigs = buildCurrentOutputConfigs();
lastAppliedEntry = { lastAppliedEntry = {
outputs: outputConfigs outputs: outputConfigs
@@ -2372,14 +2599,15 @@ Singleton {
} }
function revertChanges() { function revertChanges() {
if (CompositorService.isAqueous && aqueousPreview) {
finishAqueousPreview(false);
return;
}
const hadFormatChange = originalDisplayNameMode !== ""; const hadFormatChange = originalDisplayNameMode !== "";
const hadNiriChanges = originalNiriSettings !== null; const hadNiriChanges = originalNiriSettings !== null;
const hadHyprlandChanges = originalHyprlandSettings !== null; const hadHyprlandChanges = originalHyprlandSettings !== null;
if (hadFormatChange) { restoreDisplayNameMode();
SettingsData.displayNameMode = originalDisplayNameMode;
SettingsData.saveSettings();
}
if (hadNiriChanges) { if (hadNiriChanges) {
SessionData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings)); SessionData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings));
@@ -99,6 +99,25 @@ Item {
width: parent.width width: parent.width
} }
Column {
width: parent.width
spacing: Theme.spacingS
visible: CompositorService.isAqueous && DisplayConfigState.validationError !== ""
StyledText {
width: parent.width
text: DisplayConfigState.validationError
color: Theme.error
wrapMode: Text.WordWrap
}
DankButton {
text: I18n.tr("Discard draft and reload", "Discard unsaved Aqueous display settings and refresh the current display state")
enabled: !DisplayConfigState.validatingConfig
onClicked: DisplayConfigState.discardAqueousPreview()
}
}
StyledRect { StyledRect {
width: parent.width width: parent.width
height: profileSection.implicitHeight + Theme.spacingL * 2 height: profileSection.implicitHeight + Theme.spacingL * 2
+3 -3
View File
@@ -70,7 +70,7 @@ Item {
text: I18n.tr("Intelligent Auto-hide") text: I18n.tr("Intelligent Auto-hide")
description: I18n.tr("Show dock when floating windows don't overlap its area") description: I18n.tr("Show dock when floating windows don't overlap its area")
checked: SettingsData.dockSmartAutoHide checked: SettingsData.dockSmartAutoHide
visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango) visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous)
onToggled: checked => { onToggled: checked => {
if (checked && SettingsData.dockAutoHide) { if (checked && SettingsData.dockAutoHide) {
SettingsData.set("dockAutoHide", false); SettingsData.set("dockAutoHide", false);
@@ -83,9 +83,9 @@ Item {
settingKey: "dockOpenOnOverview" settingKey: "dockOpenOnOverview"
tags: ["dock", "overview", "niri"] tags: ["dock", "overview", "niri"]
text: I18n.tr("Show on Overview") text: I18n.tr("Show on Overview")
description: I18n.tr("Always show the dock when niri's overview is open") description: CompositorService.isAqueous ? I18n.tr("Show on Overview") : I18n.tr("Always show the dock when niri's overview is open")
checked: SettingsData.dockOpenOnOverview checked: SettingsData.dockOpenOnOverview
visible: CompositorService.isNiri visible: CompositorService.isNiri || CompositorService.isAqueous
onToggled: checked => SettingsData.set("dockOpenOnOverview", checked) onToggled: checked => SettingsData.set("dockOpenOnOverview", checked)
} }
+2 -2
View File
@@ -369,13 +369,13 @@ Item {
settingKey: "frameBarIntegration" settingKey: "frameBarIntegration"
collapsible: true collapsible: true
expanded: true expanded: true
visible: SettingsData.frameEnabled && CompositorService.isNiri visible: SettingsData.frameEnabled && (CompositorService.isNiri || CompositorService.isAqueous)
SettingsToggleRow { SettingsToggleRow {
settingKey: "frameShowOnOverview" settingKey: "frameShowOnOverview"
tags: ["frame", "overview", "show", "hide", "niri"] tags: ["frame", "overview", "show", "hide", "niri"]
text: I18n.tr("Show on Overview") text: I18n.tr("Show on Overview")
description: I18n.tr("Show during Niri overview") description: CompositorService.isAqueous ? I18n.tr("Show on Overview") : I18n.tr("Show during Niri overview")
checked: SettingsData.frameShowOnOverview checked: SettingsData.frameShowOnOverview
onToggled: checked => SettingsData.set("frameShowOnOverview", checked) onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
} }
+33 -8
View File
@@ -22,6 +22,9 @@ Item {
property bool recording: false property bool recording: false
property bool isNew: false property bool isNew: false
property bool readOnly: false property bool readOnly: false
property bool saveBlocked: false
property var retainedEdit: null
readonly property bool retainEdits: retainedEdit !== null
property string restoreKey: "" property string restoreKey: ""
property int editingKeyIndex: -1 property int editingKeyIndex: -1
@@ -70,6 +73,7 @@ Item {
signal removeBind(string key) signal removeBind(string key)
signal resetBind(string key) signal resetBind(string key)
signal cancelEdit signal cancelEdit
signal editChanged
clip: true clip: true
@@ -91,7 +95,9 @@ Item {
} }
Component.onCompleted: { Component.onCompleted: {
if (isNew && isExpanded) if (retainedEdit && isExpanded)
restoreRetainedEdit();
else if (isNew && isExpanded)
resetEdits(); resetEdits();
} }
@@ -99,6 +105,10 @@ Item {
_userToggledExpand = true; _userToggledExpand = true;
if (!isExpanded) if (!isExpanded)
return; return;
if (retainedEdit) {
restoreRetainedEdit();
return;
}
if (restoreKey) { if (restoreKey) {
restoreToKey(restoreKey); restoreToKey(restoreKey);
} else { } else {
@@ -112,6 +122,13 @@ Item {
restoreToKey(restoreKey); restoreToKey(restoreKey);
} }
function restoreRetainedEdit() {
const retained = retainedEdit;
editingKeyIndex = keys.findIndex(key => key.key === retained.originalKey);
addingNewKey = !retained.originalKey;
updateEdit(retained.data);
}
function restoreToKey(keyToFind) { function restoreToKey(keyToFind) {
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (keys[i].key === keyToFind) { if (keys[i].key === keyToFind) {
@@ -151,7 +168,8 @@ Item {
} }
hasChanges = false; hasChanges = false;
_actionType = Actions.getActionType(editAction); _actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction); useCustomCompositor = _actionType === "compositor" && editAction && !KeybindsService.isKnownCompositorAction(editAction);
editChanged();
return; return;
} }
} }
@@ -175,7 +193,8 @@ Item {
editAllowInhibiting = editingKeyIndex >= 0 ? keys[editingKeyIndex].allowInhibiting : undefined; editAllowInhibiting = editingKeyIndex >= 0 ? keys[editingKeyIndex].allowInhibiting : undefined;
hasChanges = false; hasChanges = false;
_actionType = Actions.getActionType(editAction); _actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction); useCustomCompositor = _actionType === "compositor" && editAction && !KeybindsService.isKnownCompositorAction(editAction);
editChanged();
} }
function startAddingNewKey() { function startAddingNewKey() {
@@ -187,6 +206,7 @@ Item {
editingKeyIndex = -1; editingKeyIndex = -1;
editKey = ""; editKey = "";
hasChanges = true; hasChanges = true;
editChanged();
} }
function selectKeyForEdit(index) { function selectKeyForEdit(index) {
@@ -202,6 +222,7 @@ Item {
editRepeat = keys[index].repeat; editRepeat = keys[index].repeat;
editAllowInhibiting = keys[index].allowInhibiting; editAllowInhibiting = keys[index].allowInhibiting;
hasChanges = false; hasChanges = false;
editChanged();
} }
function updateEdit(changes) { function updateEdit(changes) {
@@ -232,10 +253,11 @@ Item {
const origRepeat = hasKey ? keys[editingKeyIndex].repeat : undefined; const origRepeat = hasKey ? keys[editingKeyIndex].repeat : undefined;
const origAllowInhibiting = hasKey ? keys[editingKeyIndex].allowInhibiting : undefined; const origAllowInhibiting = hasKey ? keys[editingKeyIndex].allowInhibiting : undefined;
hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== origDesc || editCooldownMs !== origCooldown || editFlags !== origFlags || editAllowWhenLocked !== origAllowWhenLocked || editRepeat !== origRepeat || editAllowInhibiting !== origAllowInhibiting; hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== origDesc || editCooldownMs !== origCooldown || editFlags !== origFlags || editAllowWhenLocked !== origAllowWhenLocked || editRepeat !== origRepeat || editAllowInhibiting !== origAllowInhibiting;
editChanged();
} }
function canSave() { function canSave() {
if (readOnly) if (readOnly || saveBlocked)
return false; return false;
if (!editKey) if (!editKey)
return false; return false;
@@ -270,8 +292,10 @@ Item {
"repeat": editRepeat, "repeat": editRepeat,
"allowInhibiting": editAllowInhibiting "allowInhibiting": editAllowInhibiting
}); });
hasChanges = false; if (!retainEdits) {
addingNewKey = false; hasChanges = false;
addingNewKey = false;
}
} }
ShortcutInhibitor { ShortcutInhibitor {
@@ -1705,6 +1729,7 @@ Item {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM
visible: KeybindsService.currentProvider !== "aqueous"
StyledText { StyledText {
text: I18n.tr("Title") text: I18n.tr("Title")
@@ -2007,9 +2032,9 @@ Item {
buttonHeight: root._buttonHeight buttonHeight: root._buttonHeight
backgroundColor: Theme.floatingWindowFieldColor backgroundColor: Theme.floatingWindowFieldColor
textColor: Theme.surfaceText textColor: Theme.surfaceText
visible: root.hasChanges || root.isNew visible: root.hasChanges || root.isNew || root.retainEdits
onClicked: { onClicked: {
if (root.isNew) { if (root.isNew || root.retainEdits) {
root.cancelEdit(); root.cancelEdit();
} else { } else {
root.resetEdits(); root.resetEdits();
+294 -7
View File
@@ -27,8 +27,154 @@ Item {
property bool _preserveScroll: false property bool _preserveScroll: false
property string _editingKey: "" property string _editingKey: ""
property var editDraft: null
property var reviewSnapshot: null
property bool reviewingEdit: false
property bool editBusy: false
property string editError: ""
property int _editRequest: 0
property bool _editAlive: true
readonly property bool hasEditDraft: editDraft !== null
readonly property bool editInvalidated: hasEditDraft && (editDraft.provider !== KeybindsService.currentProvider || !KeybindsService.bindEditSession || editDraft.session !== KeybindsService.bindEditSession)
onEditInvalidatedChanged: {
if (!editInvalidated)
return;
_editRequest++;
editBusy = false;
reviewingEdit = true;
reviewSnapshot = null;
editError = KeybindsService.bindEditError("invalidated");
}
Component.onDestruction: {
_editAlive = false;
_editRequest++;
}
function beginEdit(binding, key) {
if (hasEditDraft || editBusy || KeybindsService.bindMutationBusy) {
ToastService.showInfo(I18n.tr("Save or discard the current edit before editing another shortcut.", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings"));
return false;
}
try {
editDraft = KeybindsService.captureBindEdit(binding, key);
editError = "";
reviewSnapshot = null;
reviewingEdit = false;
return true;
} catch (e) {
ToastService.showError(I18n.tr("Failed to load keybinds", "Aqueous shortcut editor could not load the current bindings"), AqueousService.errorMessage(String(e)), String(e));
return false;
}
}
function updateEditDraft(originalKey, data) {
if (!editDraft || editDraft.operation !== "set" || editBusy || editInvalidated)
return;
editDraft = KeybindsService.updateBindEdit(editDraft, originalKey, data);
}
function submitEdit() {
if (!editDraft || editBusy || reviewingEdit || editInvalidated || KeybindsService.bindMutationBusy)
return;
editBusy = true;
editError = "";
const token = ++_editRequest;
const complete = result => {
if (!keybindsTab._editAlive || token !== keybindsTab._editRequest)
return;
keybindsTab.editBusy = false;
if (result.success) {
const key = keybindsTab.editDraft.operation === "set" ? keybindsTab.editDraft.data.key : "";
const action = keybindsTab.editDraft.data.action;
keybindsTab.editDraft = null;
keybindsTab.reviewSnapshot = null;
keybindsTab.reviewingEdit = false;
keybindsTab._editingKey = key;
keybindsTab.expandedKey = action;
return;
}
keybindsTab.reviewSnapshot = result.snapshot || null;
keybindsTab.reviewingEdit = result.code !== "load_failed" && result.code !== "busy";
keybindsTab.editError = KeybindsService.bindEditError(result.code);
};
switch (editDraft.operation) {
case "set":
KeybindsService.saveBind(editDraft.originalKey, editDraft.data, editDraft, complete);
return;
case "remove":
KeybindsService.removeBind(editDraft.originalKey, editDraft, complete);
return;
case "reset":
KeybindsService.resetBind(editDraft.originalKey, editDraft, complete);
return;
}
}
function reloadEdit() {
if (!editDraft || editBusy || editInvalidated)
return;
editBusy = true;
reviewingEdit = true;
const token = ++_editRequest;
KeybindsService.loadBindReview((snapshot, message) => {
if (!keybindsTab._editAlive || token !== keybindsTab._editRequest)
return;
keybindsTab.editBusy = false;
keybindsTab.reviewSnapshot = snapshot;
keybindsTab.editError = message;
});
}
function acceptReview() {
if (!editDraft || !reviewSnapshot || editBusy || editInvalidated)
return;
const result = KeybindsService.reconcileBindEdit(editDraft, reviewSnapshot);
if (!result.draft) {
editError = KeybindsService.bindEditError(result.code);
return;
}
editDraft = result.draft;
reviewingEdit = false;
editError = "";
if (editDraft.operation !== "set")
confirmEditRemoval();
}
function confirmEditRemoval() {
if (!editDraft)
return;
if (editDraft.operation === "reset") {
confirmResetBind(editDraft.originalKey, "");
return;
}
confirmRemoveBind(editDraft.originalKey, "");
}
function discardEdit() {
if (editBusy || KeybindsService.bindMutationBusy)
return;
_editRequest++;
editDraft = null;
reviewSnapshot = null;
reviewingEdit = false;
editError = "";
expandedKey = "";
showingNewBind = false;
_editingKey = "";
KeybindsService.loadBinds(false);
}
function _updateFiltered() { function _updateFiltered() {
const allBinds = KeybindsService.getFlatBinds(); let allBinds = KeybindsService.getFlatBinds();
if (keybindsTab.editDraft?.action) {
const binding = keybindsTab.editDraft.binding;
const found = allBinds.some(bind => bind.action === binding.action);
allBinds = allBinds.map(bind => bind.action === binding.action ? binding : bind);
if (!found)
allBinds.push(binding);
}
if (!searchQuery && !selectedCategory) { if (!searchQuery && !selectedCategory) {
_filteredBinds = allBinds; _filteredBinds = allBinds;
return; return;
@@ -80,6 +226,14 @@ Item {
} }
function toggleExpanded(action) { function toggleExpanded(action) {
if (KeybindsService.requiresBindReview && !keybindsTab.hasEditDraft) {
const binding = KeybindsService.getFlatBinds().find(bind => bind.action === action);
if (!binding || !keybindsTab.beginEdit(binding, binding.keys[0]?.key || ""))
return;
} else if (keybindsTab.hasEditDraft && action !== keybindsTab.editDraft.action) {
ToastService.showInfo(I18n.tr("Save or discard the current edit before editing another shortcut.", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings"));
return;
}
expandedKey = expandedKey === action ? "" : action; expandedKey = expandedKey === action ? "" : action;
} }
@@ -88,27 +242,59 @@ Item {
KeybindsService.showHyprlandReadOnlyWarning(); KeybindsService.showHyprlandReadOnlyWarning();
return; return;
} }
if (KeybindsService.requiresBindReview) {
if (!keybindsTab.beginEdit({
action: "",
desc: ""
}, ""))
return;
newBindItem.resetEdits();
}
showingNewBind = true; showingNewBind = true;
expandedKey = ""; expandedKey = "";
} }
function cancelNewBind() { function cancelNewBind() {
if (keybindsTab.hasEditDraft) {
keybindsTab.discardEdit();
return;
}
showingNewBind = false; showingNewBind = false;
} }
function saveNewBind(bindData) { function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData); saveBind("", bindData);
}
function saveBind(originalKey, bindData) {
if (KeybindsService.requiresBindReview) {
keybindsTab.updateEditDraft(originalKey, bindData);
keybindsTab.submitEdit();
return;
}
KeybindsService.saveBind(originalKey, bindData);
_editingKey = bindData.key; _editingKey = bindData.key;
expandedKey = bindData.action; expandedKey = bindData.action;
} }
function confirmRemoveBind(key, remainingKey) { function confirmRemoveBind(key, remainingKey) {
const draft = KeybindsService.requiresBindReview ? prepareRemoval(key, "remove") : null;
if (KeybindsService.requiresBindReview && !draft)
return;
const baselineDraft = keybindsTab.editDraft;
removeBindConfirm.showWithOptions({ removeBindConfirm.showWithOptions({
title: I18n.tr("Remove Shortcut?"), title: I18n.tr("Remove Shortcut?"),
message: KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to dms/binds-user.lua so it stays removed across DMS updates.").arg(key) : I18n.tr("Remove the shortcut %1?").arg(key), message: KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to dms/binds-user.lua so it stays removed across DMS updates.").arg(key) : I18n.tr("Remove the shortcut %1?").arg(key),
confirmText: I18n.tr("Remove"), confirmText: I18n.tr("Remove"),
confirmColor: Theme.primary, confirmColor: Theme.primary,
onConfirm: () => { onConfirm: () => {
if (draft) {
if (keybindsTab.editDraft !== baselineDraft)
return;
keybindsTab.editDraft = draft;
keybindsTab.submitEdit();
return;
}
KeybindsService.removeBind(key); KeybindsService.removeBind(key);
keybindsTab._editingKey = remainingKey; keybindsTab._editingKey = remainingKey;
} }
@@ -116,18 +302,40 @@ Item {
} }
function confirmResetBind(key, remainingKey) { function confirmResetBind(key, remainingKey) {
const draft = KeybindsService.requiresBindReview ? prepareRemoval(key, "reset") : null;
if (KeybindsService.requiresBindReview && !draft)
return;
const baselineDraft = keybindsTab.editDraft;
removeBindConfirm.showWithOptions({ removeBindConfirm.showWithOptions({
title: I18n.tr("Reset to default"), title: I18n.tr("Reset to default"),
message: I18n.tr("Drop your override for %1 so the DMS default action re-applies?").arg(key), message: I18n.tr("Drop your override for %1 so the DMS default action re-applies?").arg(key),
confirmText: I18n.tr("Reset"), confirmText: I18n.tr("Reset"),
confirmColor: Theme.primary, confirmColor: Theme.primary,
onConfirm: () => { onConfirm: () => {
if (draft) {
if (keybindsTab.editDraft !== baselineDraft)
return;
keybindsTab.editDraft = draft;
keybindsTab.submitEdit();
return;
}
KeybindsService.resetBind(key); KeybindsService.resetBind(key);
keybindsTab._editingKey = remainingKey; keybindsTab._editingKey = remainingKey;
} }
}); });
} }
function prepareRemoval(key, operation) {
if (!hasEditDraft) {
const binding = KeybindsService.getFlatBinds().find(bind => bind.keys.some(entry => entry.key === key));
if (!binding || !beginEdit(binding, key))
return null;
}
if (editBusy || reviewingEdit || editInvalidated)
return null;
return KeybindsService.updateBindEdit(editDraft, key, null, operation);
}
function _onSaveSuccess() { function _onSaveSuccess() {
if (showingNewBind) { if (showingNewBind) {
showingNewBind = false; showingNewBind = false;
@@ -198,6 +406,11 @@ Item {
function _ensureCurrentProvider() { function _ensureCurrentProvider() {
if (!KeybindsService.available) if (!KeybindsService.available)
return; return;
if (KeybindsService.requiresBindReview) {
if (!keybindsTab.hasEditDraft && !KeybindsService.bindMutationBusy)
KeybindsService.loadBinds(false);
return;
}
const cachedProvider = KeybindsService.keybinds?.provider; const cachedProvider = KeybindsService.keybinds?.provider;
const targetProvider = KeybindsService.currentProvider; const targetProvider = KeybindsService.currentProvider;
if (cachedProvider !== targetProvider || KeybindsService._dataVersion === 0) { if (cachedProvider !== targetProvider || KeybindsService._dataVersion === 0) {
@@ -296,8 +509,8 @@ Item {
} }
StyledText { StyledText {
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf" readonly property string bindsFile: KeybindsService.requiresBindReview ? "aqueous-config" : KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
text: KeybindsService.readOnly ? I18n.tr("Hyprland conf mode is read-only in Settings") : I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile) text: KeybindsService.requiresBindReview ? I18n.tr("Click any shortcut to edit Aqueous configuration", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings") : KeybindsService.readOnly ? I18n.tr("Hyprland conf mode is read-only in Settings") : I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -508,6 +721,56 @@ Item {
} }
} }
Column {
width: Math.min(650, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
visible: keybindsTab.hasEditDraft && (keybindsTab.reviewingEdit || keybindsTab.editError !== "" || keybindsTab.editDraft.operation !== "set")
spacing: Theme.spacingM
StyledText {
width: parent.width
visible: keybindsTab.reviewingEdit || keybindsTab.editError !== ""
text: keybindsTab.editError || I18n.tr("Review the current bindings before saving this edit.", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings")
color: Theme.error
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
visible: keybindsTab.reviewingEdit && !!keybindsTab.reviewSnapshot
text: KeybindsService.describeBindReview(keybindsTab.editDraft, keybindsTab.reviewSnapshot)
color: Theme.surfaceText
wrapMode: Text.WordWrap
}
Flow {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: I18n.tr("Refresh")
enabled: !keybindsTab.editBusy && !keybindsTab.editInvalidated
onClicked: keybindsTab.reloadEdit()
}
DankButton {
text: I18n.tr("Accept reviewed changes", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings")
visible: keybindsTab.reviewingEdit && !!keybindsTab.reviewSnapshot
enabled: !keybindsTab.editBusy && !keybindsTab.editInvalidated
onClicked: keybindsTab.acceptReview()
}
DankButton {
text: I18n.tr("Discard")
enabled: !keybindsTab.editBusy && !KeybindsService.bindMutationBusy
onClicked: keybindsTab.discardEdit()
}
DankButton {
text: I18n.tr("Remove")
visible: keybindsTab.hasEditDraft && keybindsTab.editDraft.operation !== "set"
enabled: !keybindsTab.editBusy && !keybindsTab.reviewingEdit && !keybindsTab.editInvalidated
onClicked: keybindsTab.confirmEditRemoval()
}
}
}
StyledRect { StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2) width: Math.min(650, parent.width - Theme.spacingL * 2)
height: newBindSection.implicitHeight + Theme.spacingL * 2 height: newBindSection.implicitHeight + Theme.spacingL * 2
@@ -545,6 +808,7 @@ Item {
} }
KeybindItem { KeybindItem {
id: newBindItem
width: parent.width width: parent.width
isNew: true isNew: true
isExpanded: true isExpanded: true
@@ -561,6 +825,17 @@ Item {
}) })
panelWindow: keybindsTab.parentModal panelWindow: keybindsTab.parentModal
readOnly: KeybindsService.readOnly readOnly: KeybindsService.readOnly
retainedEdit: keybindsTab.hasEditDraft && keybindsTab.showingNewBind ? keybindsTab.editDraft : null
saveBlocked: retainEdits && (keybindsTab.reviewingEdit || keybindsTab.editDraft.operation !== "set")
enabled: !retainEdits || (!keybindsTab.editBusy && !keybindsTab.editInvalidated && !KeybindsService.bindMutationBusy)
onEditChanged: {
if (retainEdits)
keybindsTab.updateEditDraft("", {
key: editKey,
action: editAction,
desc: editDesc
});
}
onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData) onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData)
onCancelEdit: keybindsTab.cancelNewBind() onCancelEdit: keybindsTab.cancelNewBind()
} }
@@ -670,13 +945,25 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
bindData: modelData bindData: modelData
isExpanded: keybindsTab.expandedKey === modelData.action isExpanded: keybindsTab.expandedKey === modelData.action
retainedEdit: keybindsTab.hasEditDraft && keybindsTab.editDraft.action === modelData.action ? keybindsTab.editDraft : null
saveBlocked: retainEdits && (keybindsTab.reviewingEdit || keybindsTab.editDraft.operation !== "set")
enabled: !retainEdits || (!keybindsTab.editBusy && !keybindsTab.editInvalidated && !KeybindsService.bindMutationBusy)
onEditChanged: {
if (KeybindsService.requiresBindReview && isExpanded && hasChanges && !keybindsTab.hasEditDraft)
keybindsTab.beginEdit(bindData, addingNewKey ? "" : _originalKey);
if (retainEdits)
keybindsTab.updateEditDraft(addingNewKey ? "" : _originalKey, {
key: editKey,
action: editAction,
desc: editDesc
});
}
onCancelEdit: keybindsTab.discardEdit()
panelWindow: keybindsTab.parentModal panelWindow: keybindsTab.parentModal
readOnly: KeybindsService.readOnly readOnly: KeybindsService.readOnly
onToggleExpand: keybindsTab.toggleExpanded(modelData.action) onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => { onSaveBind: (originalKey, newData) => {
KeybindsService.saveBind(originalKey, newData); keybindsTab.saveBind(originalKey, newData);
keybindsTab._editingKey = newData.key;
keybindsTab.expandedKey = newData.action;
} }
onRemoveBind: key => { onRemoveBind: key => {
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? ""; const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
@@ -455,6 +455,17 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL spacing: Theme.spacingXL
Loader {
width: parent.width
active: CompositorService.isAqueous
sourceComponent: AqueousAppearanceSettings {
cursor: true
settingKey: "aqueousCursor"
title: I18n.tr("Aqueous cursor", "Aqueous compositor cursor synchronization settings")
visible: CompositorService.isAqueous
}
}
SettingsCard { SettingsCard {
tab: "theme" tab: "theme"
tags: ["color", "palette", "theme", "appearance"] tags: ["color", "palette", "theme", "appearance"]
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
import qs.Services
import qs.Modules.Settings.Widgets import qs.Modules.Settings.Widgets
Item { Item {
@@ -55,6 +56,16 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL spacing: Theme.spacingXL
Loader {
width: parent.width
active: CompositorService.isAqueous
sourceComponent: AqueousAppearanceSettings {
settingKey: "aqueousTypography"
title: I18n.tr("Aqueous typography", "Aqueous compositor font synchronization settings")
visible: CompositorService.isAqueous
}
}
SettingsCard { SettingsCard {
tab: "typography" tab: "typography"
tags: ["font", "family", "text", "typography"] tags: ["font", "family", "text", "typography"]
@@ -0,0 +1,199 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
SettingsCard {
id: root
property bool cursor: false
property var snapshot: null
property var changes: ({})
property bool busy: false
property string error: ""
readonly property var report: cursor ? snapshot?.desktop_cursor : snapshot?.desktop_typography
readonly property bool partial: (report?.failed_count || 0) > 0
readonly property bool supported: snapshot?.capabilities?.includes(cursor ? "cursor_sync" : "typography_sync") || false
title: cursor ? I18n.tr("Aqueous cursor", "Aqueous compositor cursor synchronization settings") : I18n.tr("Aqueous typography", "Aqueous compositor font synchronization settings")
iconName: cursor ? "mouse" : "text_fields"
tab: cursor ? "theme" : "typography"
tags: ["aqueous", "appearance"]
function reload() {
if (busy || AqueousConfigService.busy)
return;
busy = true;
AqueousConfigService.load((data, message) => {
busy = false;
error = message ? AqueousService.errorMessage(message) : "";
if (!data)
return;
snapshot = data;
changes = ({});
if (!supported)
error = I18n.tr("Unavailable");
});
}
function targetStatus(target) {
switch (target.state) {
case "synced":
return I18n.tr("Synchronized", "Aqueous appearance settings match this application or toolkit");
case "drifted":
return I18n.tr("Not synchronized", "Aqueous appearance settings differ from this application or toolkit");
case "partial":
return I18n.tr("Partially synchronized", "The application or toolkit cannot represent every Aqueous font setting");
case "failed":
return I18n.tr("Sync failed", "Applying Aqueous appearance settings to this application or toolkit failed");
case "unavailable":
return I18n.tr("Unavailable");
case "unmanaged":
return I18n.tr("Disabled");
default:
return I18n.tr("Unknown");
}
}
function value(id) {
if (Object.prototype.hasOwnProperty.call(changes, id))
return changes[id];
return snapshot?.fields?.find(f => f.id === id)?.value;
}
function stage(id, value) {
const updated = Object.assign({}, changes);
updated[id] = value;
changes = updated;
}
function apply(retry) {
if (busy || !snapshot || AqueousConfigService.busy)
return;
const capability = cursor ? "cursor_sync" : "typography_sync";
if (!snapshot.capabilities?.includes(capability))
return;
const draft = {
expected_generation: snapshot.generation,
create_user_override: true,
changes: retry ? [] : Object.keys(changes).map(id => ({
id: id,
value: changes[id]
}))
};
draft[cursor ? "sync_cursor" : "sync_typography"] = true;
busy = true;
AqueousConfigService.apply(draft, (data, message) => {
busy = false;
error = message ? AqueousService.errorMessage(message) : "";
if (!data)
return;
snapshot = data;
if (!retry)
changes = ({});
if (cursor)
return;
const typography = data.desktop_typography;
if (!typography || typeof typography.family !== "string" || !Number.isFinite(typography.weight) || !Number.isFinite(typography.size_pt) || typography.size_pt <= 0)
return;
SettingsData.set("fontFamily", typography.family);
SettingsData.set("fontWeight", typography.weight);
SettingsData.set("fontScale", typography.size_pt * 96 / 72 / 14);
});
}
Component.onCompleted: reload()
Column {
width: parent.width
spacing: Theme.spacingM
SettingsDropdownRow {
width: parent.width
visible: root.cursor
enabled: root.supported && !root.busy
text: I18n.tr("Cursor Theme")
options: root.snapshot?.desktop_cursor?.themes || []
currentValue: root.value("desktop.cursor.theme") || "default"
onValueChanged: value => {
root.stage("desktop.cursor.theme", value);
root.stage("desktop.cursor.managed", true);
}
}
SettingsDropdownRow {
width: parent.width
visible: !root.cursor
enabled: root.supported && !root.busy
text: I18n.tr("Normal Font")
options: root.snapshot?.desktop_typography?.families || []
currentValue: root.value("desktop.font.family") || "sans-serif"
onValueChanged: value => {
root.stage("desktop.font.family", value);
root.stage("desktop.font.style", "");
}
}
SettingsSliderRow {
width: parent.width
enabled: root.supported && !root.busy
text: root.cursor ? I18n.tr("Cursor Size") : I18n.tr("Font Size")
minimum: root.cursor ? 12 : 6
maximum: root.cursor ? 128 : 30
value: root.value(root.cursor ? "desktop.cursor.size" : "desktop.font.size_pt") || (root.cursor ? 24 : 12)
unit: root.cursor ? I18n.tr("px", "Cursor size unit, pixels") : I18n.tr("pt", "Font size unit, points")
onSliderValueChanged: value => {
root.stage(root.cursor ? "desktop.cursor.size" : "desktop.font.size_pt", value);
if (root.cursor)
root.stage("desktop.cursor.managed", true);
}
}
StyledText {
width: parent.width
visible: root.error !== "" || root.partial
text: root.error || I18n.tr("Error")
color: Theme.error
wrapMode: Text.WordWrap
}
Repeater {
model: root.report?.targets || []
StyledText {
required property var modelData
width: parent.width
text: I18n.tr("%1: %2", "Aqueous appearance sync target and its status").arg(modelData.id).arg(root.targetStatus(modelData))
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
StyledText {
width: parent.width
visible: !root.cursor
text: I18n.tr("DMS uses the font family, weight and scale. Exact face, slant, width and separately scaled bars may differ.", "Aqueous font synchronization, describing which font settings DMS can represent")
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Flow {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: I18n.tr("Apply Changes")
enabled: root.supported && !root.busy && !AqueousConfigService.busy && Object.keys(root.changes).length > 0
onClicked: root.apply(false)
}
DankButton {
text: I18n.tr("Retry")
enabled: root.supported && !root.busy && !AqueousConfigService.busy && root.partial
onClicked: root.apply(true)
}
DankButton {
text: I18n.tr("Refresh")
enabled: !root.busy && !AqueousConfigService.busy
onClicked: root.reload()
}
}
}
}
@@ -271,7 +271,7 @@ Column {
menuRoot.closed(); menuRoot.closed();
} }
BackgroundEffect.blurRegion: visible ? menuBlurRegion : null BackgroundEffect.blurRegion: visible && BlurService.enabled ? menuBlurRegion : null
Region { Region {
id: menuBlurRegion id: menuBlurRegion
@@ -183,7 +183,7 @@ SettingsCard {
"label": I18n.tr("Custom", "workspace color option") "label": I18n.tr("Custom", "workspace color option")
})] })]
readonly property bool workspaceStateColorsVisible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango readonly property bool workspaceStateColorsVisible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
readonly property bool urgentWorkspaceColorsVisible: workspaceStateColorsVisible || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle readonly property bool urgentWorkspaceColorsVisible: workspaceStateColorsVisible || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
function isFocusedAppearanceSection(section) { function isFocusedAppearanceSection(section) {
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -61,7 +62,8 @@ Item {
text: I18n.tr("Show Workspace Apps") text: I18n.tr("Show Workspace Apps")
description: I18n.tr("Display application icons in workspace indicators") description: I18n.tr("Display application icons in workspace indicators")
checked: SettingsData.showWorkspaceApps checked: SettingsData.showWorkspaceApps
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
enabled: !CompositorService.isAqueous || (AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1")
onToggled: checked => SettingsData.set("showWorkspaceApps", checked) onToggled: checked => SettingsData.set("showWorkspaceApps", checked)
} }
@@ -153,7 +155,7 @@ Item {
text: I18n.tr("Follow Monitor Focus") text: I18n.tr("Follow Monitor Focus")
description: I18n.tr("Show workspaces of the currently focused monitor") description: I18n.tr("Show workspaces of the currently focused monitor")
checked: SettingsData.workspaceFollowFocus checked: SettingsData.workspaceFollowFocus
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isAqueous
onToggled: checked => SettingsData.set("workspaceFollowFocus", checked) onToggled: checked => SettingsData.set("workspaceFollowFocus", checked)
} }
@@ -163,7 +165,8 @@ Item {
text: I18n.tr("Show Occupied Workspaces Only") text: I18n.tr("Show Occupied Workspaces Only")
description: I18n.tr("Display only workspaces that contain windows") description: I18n.tr("Display only workspaces that contain windows")
checked: SettingsData.showOccupiedWorkspacesOnly checked: SettingsData.showOccupiedWorkspacesOnly
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
enabled: !CompositorService.isAqueous || (AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1")
onToggled: checked => SettingsData.set("showOccupiedWorkspacesOnly", checked) onToggled: checked => SettingsData.set("showOccupiedWorkspacesOnly", checked)
} }
@@ -173,7 +176,7 @@ Item {
text: I18n.tr("Reverse Scrolling Direction") text: I18n.tr("Reverse Scrolling Direction")
description: I18n.tr("Reverse workspace switch direction when scrolling over the bar") description: I18n.tr("Reverse workspace switch direction when scrolling over the bar")
checked: SettingsData.reverseScrolling checked: SettingsData.reverseScrolling
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
onToggled: checked => SettingsData.set("reverseScrolling", checked) onToggled: checked => SettingsData.set("reverseScrolling", checked)
} }
@@ -0,0 +1,149 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("AqueousConfigService")
property bool busy: false
function requireCapabilities(result, required) {
if (result?.protocol !== 1 || !Array.isArray(result.capabilities))
throw new Error("unsupported aqueous-config protocol or discovery");
for (const capability of required) {
if (!result.capabilities.includes(capability))
throw new Error("unsupported aqueous-config capability: " + capability);
}
}
function helper(operation, input, callback) {
const args = ["aqueous-config", operation, "--shell", "dms"];
if (input !== null)
args.push("--request", "-");
AqueousService.runJson(args, input, (result, error) => {
if (!error && result?.ok !== true)
error = result?.code ? result.code + ": " + (result.message || "") : "aqueous-config rejected the request";
callback(result, error);
});
}
function request(operation, draft, callback) {
if (!CompositorService.isAqueous || busy) {
callback(null, busy ? "busy" : "unavailable");
return;
}
busy = true;
const complete = (result, error) => {
busy = false;
if (error)
log.warn("helper operation failed:", operation, error);
callback(error ? null : result, error);
};
if (operation === "snapshot") {
helper("snapshot", null, (result, error) => {
try {
if (error)
throw new Error(error);
requireCapabilities(result, ["schema_fields", "shell_dms"]);
if (typeof result.generation !== "string" || !result.generation)
throw new Error("missing configuration generation");
complete(result, "");
} catch (e) {
complete(null, String(e));
}
});
return;
}
if (!["apply", "validate"].includes(operation) || typeof draft?.expected_generation !== "string" || !draft.expected_generation) {
complete(null, "invalid operation or missing expected_generation; reload configuration");
return;
}
const request = Object.assign({}, draft, {
protocol: 1
});
request.backup_dir = request.backup_dir || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/DankMaterialShell/aqueous-backups";
const input = JSON.stringify(request);
const required = ["validate", "stdin_requests", "generation_check", "atomic_file_replace", "shell_dms"];
for (const [key, capability] of Object.entries({
monitor_changes: "monitor_modes",
custom_keybind_changes: "keybinds",
sync_cursor: "cursor_sync",
sync_typography: "typography_sync"
})) {
if (Object.prototype.hasOwnProperty.call(request, key))
required.push(capability);
}
helper("version", null, (version, error) => {
try {
if (error)
throw new Error(error);
requireCapabilities(version, required);
} catch (e) {
complete(null, String(e));
return;
}
helper("validate", input, (validated, error) => {
if (error || operation === "validate") {
complete(validated, error);
return;
}
helper("apply", input, complete);
});
});
}
function buildOutputsConfig(snapshot, outputs, original) {
if (!snapshot.capabilities?.includes("monitor_modes"))
throw new Error("unsupported: monitor_modes");
const raw = (snapshot.raw_files?.outputs || "") + "\n" + (snapshot.raw_files?.wm || "");
if (/^\s*edid\s*=/m.test(raw))
throw new Error("unsupported: this aqueous-config version does not expose EDID monitor edits");
const transforms = ["normal", "90", "180", "270", "flipped", "flipped-90", "flipped-180", "flipped-270"];
const changes = [];
if ((snapshot.monitors || []).some(m => /[*?\[]/.test(m.name)))
throw new Error("unsupported: reconcile wildcard monitor configuration before persisting displays");
for (const output of outputs) {
const before = original.find(o => o.name === output.name);
if (!before)
throw new Error("conflict: output changed during preview");
if (output.enabled !== before.enabled)
throw new Error("unsupported: this aqueous-config version cannot persist output enablement");
if (output.adaptiveSync !== before.adaptiveSync)
throw new Error("unsupported: this aqueous-config version cannot persist adaptive sync");
if (!output.enabled)
continue;
const configured = (snapshot.monitors || []).filter(m => m.name === output.name);
if (configured.length > 1)
throw new Error("conflict: multiple configured monitor entries");
const monitor = configured[0];
if (Math.abs(output.scale - (monitor?.scale ?? 1)) > 0.0001)
throw new Error("unsupported: this aqueous-config version cannot persist output scale");
const mode = output.currentMode;
if (!mode)
throw new Error("unavailable: output mode");
changes.push({
id: monitor?.id || "live:" + output.name,
name: output.name,
x: output.x,
y: output.y,
transform: transforms[output.transform],
mode: mode.width + "x" + mode.height + "@" + (mode.refresh / 1000)
});
}
return {
expected_generation: snapshot.generation,
monitor_changes: changes,
create_user_override: true
};
}
function load(callback) {
request("snapshot", null, callback);
}
function apply(draft, callback) {
request("apply", draft, callback);
}
}
+970
View File
@@ -0,0 +1,970 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../Common/AqueousIpc.js" as Ipc
Singleton {
id: root
readonly property var log: Log.scoped("AqueousService")
property var state: ({})
property string seatName: ""
readonly property bool enabled: CompositorService.isAqueous
readonly property bool available: enabled && requestConnection.ready && eventConnection.ready && state.available === true
property var discoveredCapabilities: ({})
property var pendingProcesses: []
property int lifecycle: 0
readonly property string socketPath: Quickshell.env("AQUEOUS_SOCKET")
readonly property string runtimePath: Quickshell.env("XDG_RUNTIME_DIR")
readonly property bool validSocketPath: Ipc.socketPath(socketPath, runtimePath)
property bool watching: false
property bool recovering: false
property bool reconnecting: false
property int reconnectAttempt: 0
property var commandQueue: []
property var currentCommand: null
readonly property bool commandPending: currentCommand !== null
readonly property int maxBatchBytes: 4 * 1024 * 1024
readonly property int maxModelBytes: 2 * 1024 * 1024
readonly property var capabilities: discoveredCapabilities
readonly property string session: available ? state.batch.session : ""
readonly property string sequence: available ? state.batch.sequence : ""
readonly property var entities: available ? state.batch.upsert : []
readonly property var outputs: entities.filter(e => e.kind === "output")
readonly property var workspaces: entities.filter(e => e.kind === "workspace").map(w => Object.assign({}, w, {
aqueousSession: session
}))
readonly property var windows: entities.filter(e => e.kind === "window")
readonly property var seats: entities.filter(e => e.kind === "seat")
readonly property var sessionState: entities.find(e => e.kind === "session") || ({})
readonly property bool locked: sessionState.locked === true
readonly property var seat: seatName ? seats.find(s => s.id === seatName) : (seats.length === 1 ? seats[0] : null)
readonly property var keyboard: entities.find(e => e.kind === "keyboard" && e.id === seat?.keyboard)
readonly property var keyboardLayouts: keyboard?.layouts || []
readonly property string keyboardLayout: keyboardLayouts[keyboard?.index] || ""
readonly property string focusedOutput: outputs.find(o => o.id === seat?.output)?.name || ""
readonly property bool inOverview: !!sessionState.overview_output
readonly property var focusedWindow: {
const window = windows.find(w => w.id === seat?.window);
return window ? windowFacade(window) : null;
}
readonly property var toplevels: windows.filter(w => taskbarEligible(w)).map(w => windowFacade(w))
function taskbarEligible(window) {
return !window.skip_taskbar && (window.visible || window.minimized || !workspaces.find(ws => ws.id === window.workspace)?.active);
}
function outputId(name) {
return outputs.find(o => o.name === name)?.id || "";
}
function workspacesForOutput(name) {
const id = outputId(name || focusedOutput);
return workspaces.filter(w => w.output === id).sort((a, b) => a.number - b.number);
}
function parseJson(text) {
if (Ipc.byteLength(text) > maxBatchBytes + (text.endsWith("\n") ? 1 : 0))
throw new Error("JSON exceeds 4 MiB");
return JSON.parse(text);
}
function validateEntity(entity) {
const fields = {
output: "id:s name:s bounds:r usable_bounds:r scale:n transform:s active_workspace:? enabled:b powered:b",
workspace: "id:s output:s name:s number:i active:b urgent:b",
window: "id:s backend:s app_id:? class:? title:? workspace:? output:? geometry:r outer_geometry:r layout:s focused:b visible:b floating:b minimized:b maximized:b fullscreen:b skip_taskbar:b skip_switcher:b always_above:b always_below:b snapped:b fixed_position:b can_minimize:b can_maximize:b can_activate:b",
seat: "id:s output:? window:? focus_kind:s keyboard:?",
keyboard: "id:s seat:s layouts:a index:i",
keyboard_device: "id:s name:? seat:s group:? virtual:b",
session: "id:s locked:b default_seat:? overview_output:? overview_window:?"
};
if (!entity || !Object.prototype.hasOwnProperty.call(fields, entity.kind) || typeof entity.id !== "string" || !entity.id)
throw new Error("invalid entity kind or ID");
for (const field of fields[entity.kind].split(" ")) {
const [name, type] = field.split(":");
const value = entity[name];
let valid = false;
switch (type) {
case "s":
valid = typeof value === "string";
break;
case "?":
valid = value === null || typeof value === "string";
break;
case "b":
valid = typeof value === "boolean";
break;
case "i":
valid = Number.isSafeInteger(value);
break;
case "n":
valid = typeof value === "number" && Number.isFinite(value);
break;
case "a":
valid = Array.isArray(value) && value.every(v => typeof v === "string");
break;
case "r":
valid = value && ["x", "y", "width", "height"].every(k => Number.isSafeInteger(value[k]));
break;
}
if (!valid)
throw new Error("invalid " + entity.kind + "." + name);
}
if ((entity.kind === "session" && entity.id !== "session") || (entity.kind === "workspace" && entity.number < 1) || (entity.kind === "output" && entity.scale <= 0) || (entity.kind === "keyboard" && (entity.index < 0 || (entity.layouts.length && entity.index >= entity.layouts.length))) || (entity.kind === "window" && !["xdg", "xwayland"].includes(entity.backend)) || (entity.kind === "seat" && !["window", "shell_surface", "layer_surface", "override_redirect", "lock_surface", "none"].includes(entity.focus_kind)))
throw new Error("invalid " + entity.kind);
}
function reduceBatch(previous, batch) {
if (!batch || batch.schema !== 1 || typeof batch.session !== "string" || !/^[0-9a-f]{32}$/.test(batch.session) || typeof batch.sequence !== "string" || !/^[0-9]+$/.test(batch.sequence) || !Array.isArray(batch.upsert) || !Array.isArray(batch.removed))
throw new Error("invalid shell batch envelope");
if (batch.type !== "snapshot" && batch.type !== "delta")
throw new Error("invalid shell batch type");
if (batch.type === "snapshot" && (batch.base_sequence !== null || batch.removed.length))
throw new Error("invalid snapshot baseline");
if (batch.type === "delta" && (!previous || batch.session !== previous.session || batch.base_sequence !== previous.sequence || batch.sequence === previous.sequence))
throw new Error("shell delta continuity mismatch");
const model = Object.assign({}, batch.type === "delta" ? previous.model : {});
const entityBytes = Object.assign({}, batch.type === "delta" ? previous.entityBytes : {});
let modelBytes = batch.type === "delta" ? previous.modelBytes : 1;
const seen = new Set();
for (const entity of batch.upsert) {
validateEntity(entity);
const key = entity.kind + ":" + entity.id;
if (seen.has(key))
throw new Error("duplicate entity key");
seen.add(key);
const size = Ipc.byteLength(JSON.stringify(key) + ":" + JSON.stringify(entity)) + 1;
modelBytes += size - (entityBytes[key] || 0);
entityBytes[key] = size;
model[key] = entity;
}
for (const key of batch.removed) {
if (typeof key !== "string" || !/^(output|workspace|window|seat|keyboard|keyboard_device|session):.+$/.test(key) || seen.has(key))
throw new Error("invalid or duplicate removal");
seen.add(key);
modelBytes -= entityBytes[key] || 0;
delete entityBytes[key];
delete model[key];
}
if (!model["session:session"])
throw new Error("missing session entity");
const references = {
output: {
active_workspace: "workspace"
},
workspace: {
output: "output"
},
window: {
output: "output",
workspace: "workspace"
},
seat: {
output: "output",
window: "window",
keyboard: "keyboard"
},
keyboard: {
seat: "seat"
},
keyboard_device: {
seat: "seat",
group: "keyboard"
},
session: {
default_seat: "seat",
overview_output: "output",
overview_window: "window"
}
};
for (const entity of Object.values(model)) {
for (const [field, kind] of Object.entries(references[entity.kind])) {
if (entity[field] !== null && !model[kind + ":" + entity[field]])
throw new Error("dangling " + entity.kind + "." + field);
}
}
if (modelBytes > maxModelBytes)
throw new Error("shell model exceeds size bound");
return {
session: batch.session,
sequence: batch.sequence,
model: model,
entityBytes: entityBytes,
modelBytes: modelBytes,
upsert: Object.values(model)
};
}
function acceptBatch(batch) {
if (batch.session !== capabilities.session)
throw new Error("shell session changed after discovery");
const next = reduceBatch(state.available ? state.batch : null, batch);
reconnectAttempt = 0;
state = {
available: true,
batch: next
};
}
function commandPayload(action, fields) {
const request = Object.assign({
session: session,
seat: seat?.id || ""
}, fields || {});
if (!available || request.session !== session)
throw new Error("unavailable: stale session");
if (locked)
throw new Error("locked");
const [kind, operation] = action.split(".");
const capability = kind === "keyboard" ? "keyboard" : kind === "overview" ? "overview" : "commands";
if (!capabilities[capability])
throw new Error("unsupported: " + capability);
for (const key of ["id", "seat", "group", "output", "workspace", "name"]) {
if (request[key] !== undefined && (typeof request[key] !== "string" || Ipc.byteLength(request[key]) > 1024))
throw new Error("invalid: command string");
}
const target = entities.find(e => e.kind === kind && e.id === request.id);
if ((kind === "window" || kind === "workspace") && !target)
throw new Error("not_found");
const selectedSeat = request.seat ? seats.find(s => s.id === request.seat) : (seats.length === 1 ? seats[0] : null);
const output = outputs.find(o => o.id === request.output);
let payload;
switch (action) {
case "window.activate":
case "workspace.activate":
if (!selectedSeat)
throw new Error(request.seat ? "not_found: seat" : "ambiguous_seat");
if (kind === "window" && !target.can_activate)
throw new Error("unavailable: window activation");
payload = {
id: target.id,
seat: selectedSeat.id
};
break;
case "window.close":
payload = {
id: target.id
};
break;
case "window.minimized":
case "window.maximized":
case "window.fullscreen":
if (typeof request.value !== "boolean")
throw new Error("invalid: missing state");
if (operation !== "fullscreen" && !target["can_" + operation.slice(0, -1)])
throw new Error("unavailable: window state");
payload = {
id: target.id,
value: request.value
};
break;
case "window.move":
if (!!request.workspace === !!request.output)
throw new Error("invalid: move destination");
if (request.workspace && !workspaces.some(w => w.id === request.workspace))
throw new Error("not_found: workspace");
if (request.output && !output)
throw new Error("not_found: output");
payload = {
id: target.id
};
payload[request.workspace ? "workspace" : "output"] = request.workspace || output.id;
break;
case "workspace.rename":
if (typeof request.name !== "string" || /[\r\n]/.test(request.name))
throw new Error("invalid: workspace name");
payload = {
id: target.id,
name: request.name
};
break;
case "keyboard.set":
case "keyboard.next":
{
if (!selectedSeat)
throw new Error(request.seat ? "not_found: seat" : "ambiguous_seat");
const group = entities.find(e => e.kind === "keyboard" && e.id === (request.group || selectedSeat.keyboard) && e.seat === selectedSeat.id);
if (!group)
throw new Error("not_found: keyboard group");
payload = {
seat: selectedSeat.id,
group: group.id
};
if (operation === "set") {
if (!Number.isInteger(request.index) || request.index < 0 || request.index >= group.layouts.length)
throw new Error("invalid: keyboard index");
payload.index = request.index;
}
break;
}
case "overview.show":
case "overview.toggle":
case "overview.hide":
payload = {};
if (operation !== "hide") {
if (!output)
throw new Error("not_found: output");
payload.output = output.id;
}
break;
case "session.exit":
payload = {};
break;
default:
throw new Error("unsupported: action");
}
return {
action: action,
fields: payload
};
}
function commandResult(action, result, error) {
if (error)
return error;
const status = action === "window.close" || action === "session.exit" ? "accepted" : "applied";
if (result?.status !== status || (status === "applied" || result.sequence !== undefined) && (typeof result.sequence !== "string" || !/^[0-9]{1,20}$/.test(result.sequence)))
return result?.status || "missing command acknowledgement";
return "";
}
function reportCommand(action, result, error, callback) {
const failure = commandResult(action, result, error);
if (failure) {
log.warn("command failed:", action, failure);
ToastService.showError(I18n.tr("Error"), errorMessage(failure), failure);
}
if (callback)
callback(!failure, failure || result.status);
}
function errorMessage(error) {
const code = String(error).replace(/^Error: /, "").split(":")[0];
switch (code) {
case "busy":
return I18n.tr("Another Aqueous operation is in progress.", "Aqueous request rejected because another operation is running");
case "locked":
case "unavailable":
case "not_found":
return I18n.tr("Unavailable");
case "unsupported":
return I18n.tr("This operation is not supported by the installed Aqueous version.", "Aqueous compositor or configuration helper lacks a required capability");
case "conflict":
case "external_change":
case "stale_session":
return I18n.tr("Configuration changed. Refresh to continue.", "Aqueous configuration or compositor session changed while editing");
case "uncertain":
case "command completion uncertain":
return I18n.tr("The result is unknown. Refresh before trying again.", "An Aqueous operation may have completed without a reply");
default:
return I18n.tr("The Aqueous operation failed.", "Fallback error for an Aqueous compositor or configuration request");
}
}
function command(action, fields, callback) {
try {
commandPayload(action, fields);
if (commandQueue.length >= 32)
throw new Error("busy: command queue full");
} catch (e) {
reportCommand(action, null, String(e), callback);
return;
}
commandQueue = commandQueue.concat([
{
action: action,
fields: Object.assign({
session: session,
seat: seat?.id || ""
}, fields || {}),
callback: callback,
generation: lifecycle,
queuedAt: Date.now()
}
]);
drainCommands();
}
function drainCommands() {
if (currentCommand || recovering)
return;
while (commandQueue.length) {
const next = commandQueue[0];
commandQueue = commandQueue.slice(1);
try {
if (next.generation !== lifecycle || Date.now() - next.queuedAt >= 5000)
throw new Error("unavailable: queued command expired before sending");
const payload = commandPayload(next.action, next.fields);
currentCommand = next;
requestConnection.request("command", payload);
return;
} catch (e) {
currentCommand = null;
reportCommand(next.action, null, String(e), next.callback);
}
}
}
function completeCommand(result, error) {
const current = currentCommand;
currentCommand = null;
if (!current || current.generation !== lifecycle)
return;
if (!error && commandResult(current.action, result, "")) {
reportCommand(current.action, null, "command completion uncertain: invalid acknowledgement", current.callback);
disconnected("invalid command acknowledgement");
return;
}
reportCommand(current.action, result, error, current.callback);
if (error.startsWith("stale_session:")) {
disconnected(error);
return;
}
drainCommands();
}
function failCommands(error) {
const current = currentCommand;
const queued = commandQueue;
currentCommand = null;
commandQueue = [];
if (current)
reportCommand(current.action, null, "command completion uncertain: " + error, current.callback);
for (const command of queued)
reportCommand(command.action, null, "unavailable: command not sent: " + error, command.callback);
}
function runJson(args, input, callback) {
if (!enabled) {
callback(null, "unavailable");
return;
}
if (input !== null && Ipc.byteLength(input) > maxBatchBytes) {
callback(null, "request exceeds 4 MiB");
return;
}
const process = jsonProcessComponent.createObject(root, {
command: args,
input: input,
callback: callback
});
pendingProcesses = pendingProcesses.concat([process]);
process.running = true;
process.deadline.start();
}
function startWatching() {
if (!enabled)
return;
if (!validSocketPath) {
state = {
error: "unavailable: missing or invalid AQUEOUS_SOCKET"
};
return;
}
state = {
error: "unavailable: connecting to Aqueous IPC"
};
watching = true;
}
function handshakesReady() {
if (!requestConnection.ready || !eventConnection.ready)
return;
const first = requestConnection.handshake;
const second = eventConnection.handshake;
if (first.session !== second.session || Object.keys(first.capabilities).some(key => first.capabilities[key] !== second.capabilities[key]) || first.schema !== second.schema) {
disconnected("IPC connection sessions or capabilities disagree");
return;
}
discoveredCapabilities = Object.assign({}, first.capabilities, {
session: first.session,
schema: first.schema,
max_batch_bytes: Math.min(first.max_batch_bytes, second.max_batch_bytes)
});
try {
eventConnection.subscribe();
} catch (e) {
disconnected(String(e));
}
}
function disconnected(error) {
if (recovering || !watching)
return;
recovering = true;
lifecycle++;
state = {
error: error
};
discoveredCapabilities = ({});
reconnecting = true;
requestConnection.clear();
eventConnection.clear();
const delay = Math.min(400 * Math.pow(2, Math.min(reconnectAttempt++, 6)), 15000);
reconnectTimer.interval = delay + Math.floor(Math.random() * delay / 4);
reconnectTimer.restart();
failCommands(error);
recovering = false;
}
function stopWatching() {
recovering = true;
lifecycle++;
watching = false;
reconnectTimer.stop();
reconnecting = false;
reconnectAttempt = 0;
requestConnection.clear();
eventConnection.clear();
state = ({});
discoveredCapabilities = ({});
failCommands("backend changed or shell stopped");
for (const process of pendingProcesses.slice())
process.finish(null, "unavailable: backend changed or shell stopped");
recovering = false;
}
onEnabledChanged: {
stopWatching();
startWatching();
}
Component.onCompleted: startWatching()
Component.onDestruction: stopWatching()
IpcConnection {
id: requestConnection
path: root.socketPath
connected: root.watching && !root.reconnecting
onHelloReceived: root.handshakesReady()
onReply: (result, error) => root.completeCommand(result, error)
onFailed: error => root.disconnected(error)
}
IpcConnection {
id: eventConnection
path: root.socketPath
connected: root.watching && !root.reconnecting
events: true
onHelloReceived: root.handshakesReady()
onBatchReceived: batch => {
try {
root.acceptBatch(batch);
} catch (e) {
root.disconnected(String(e));
}
}
onFailed: error => root.disconnected(error)
}
Timer {
id: reconnectTimer
onTriggered: root.reconnecting = false
}
component IpcConnection: DankSocket {
id: connection
property bool events: false
property var handshake: null
property var pending: null
property string lastId: "0"
property bool subscribed: false
property bool installed: false
property string lastDelivery: ""
property var frameParser: null
readonly property bool ready: linkUp && handshake !== null
signal helloReceived
signal batchReceived(var batch)
signal reply(var result, string error)
signal failed(string error)
function clear() {
const previousParser = frameParser;
frameParser = null;
if (previousParser)
previousParser.destroy();
deadline.stop();
initialDeadline.stop();
pending = null;
handshake = null;
subscribed = false;
installed = false;
lastId = "0";
lastDelivery = "";
}
function fail(error) {
if (connected && !root.recovering)
failed(error);
}
function request(op, params) {
if (!linkUp || pending || (op !== "hello" && !handshake))
throw new Error("unavailable: IPC connection");
const id = Ipc.nextId(lastId);
const message = {
ipc: 1,
id: id,
op: op,
params: params
};
if (op !== "hello")
message.session = handshake.session;
const text = JSON.stringify(message);
if (Ipc.byteLength(text) > (handshake?.max_request_bytes || 65536))
throw new Error("invalid: IPC request exceeds size bound");
lastId = id;
pending = {
id: id,
op: op,
params: params
};
deadline.restart();
send(text);
}
function receive(line) {
try {
const value = Ipc.envelope(line, handshake?.max_frame_bytes || 4259840);
if (value.event !== undefined) {
if (!events || !subscribed || pending || value.event !== "state" || !Ipc.decimal(value.delivery) || value.delivery === lastDelivery || value.id !== undefined || value.ok !== undefined || !Ipc.object(value.batch))
throw new Error("unexpected IPC state event");
if (!installed && value.batch.type !== "snapshot")
throw new Error("missing initial snapshot");
if (value.batch.session !== handshake.session || Ipc.byteLength(JSON.stringify(value.batch)) > handshake.max_batch_bytes)
throw new Error("invalid IPC batch identity or size");
batchReceived(value.batch);
if (!ready || root.recovering)
return;
installed = true;
initialDeadline.stop();
lastDelivery = value.delivery;
request("ack", {
delivery: value.delivery
});
return;
}
if (!pending)
throw new Error("unsolicited IPC response");
const error = Ipc.response(value, pending.id);
const operation = pending;
pending = null;
deadline.stop();
if (operation.op === "command") {
reply(value.result || null, error);
return;
}
if (error)
throw new Error(error);
switch (operation.op) {
case "hello":
handshake = Ipc.hello(value.result);
helloReceived();
break;
case "subscribe":
if (value.result.subscribed !== true)
throw new Error("invalid subscription response");
subscribed = true;
break;
case "ack":
if (value.result.acked !== operation.params.delivery)
throw new Error("invalid ack response");
break;
default:
throw new Error("unexpected IPC operation");
}
} catch (e) {
fail(String(e));
}
}
function subscribe() {
initialDeadline.restart();
request("subscribe", {});
}
onConnectionStateChanged: {
clear();
if (root.recovering)
return;
if (!linkUp) {
fail("IPC stream disconnected");
return;
}
frameParser = parserComponent.createObject(connection);
try {
request("hello", {});
} catch (e) {
fail(String(e));
}
}
parser: frameParser
Component {
id: parserComponent
SplitParser {
id: lineParser
onRead: line => {
if (connection.frameParser === lineParser)
connection.receive(line);
}
}
}
Timer {
id: deadline
interval: 5000
onTriggered: connection.fail("IPC request timed out")
}
Timer {
id: initialDeadline
interval: 8000
onTriggered: connection.fail("IPC initial snapshot timed out")
}
}
function windowFacade(window) {
const windowSession = session;
const output = outputs.find(o => o.id === window.output);
return {
id: window.id,
aqueousWindowId: window.id,
aqueousKey: windowSession + ":window:" + window.id,
aqueousSession: windowSession,
aqueousWorkspaceId: window.workspace,
aqueousOutputId: window.output,
appId: window.app_id || window.class || "",
title: window.title || "",
activated: root.seat?.window === window.id,
get fullscreen() {
return window.fullscreen;
},
set fullscreen(value) {
root.command("window.fullscreen", {
id: window.id,
session: windowSession,
value: value
});
},
get maximized() {
return window.maximized;
},
set maximized(value) {
root.command("window.maximized", {
id: window.id,
session: windowSession,
value: value
});
},
visible: window.visible,
skipSwitcher: window.skip_switcher,
canMinimize: window.can_minimize,
canMaximize: window.can_maximize,
canActivate: window.can_activate,
screens: Quickshell.screens.filter(s => s.name === output?.name),
get minimized() {
return window.minimized;
},
set minimized(value) {
root.command("window.minimized", {
id: window.id,
session: windowSession,
value: value
});
},
activate: function () {
root.command("window.activate", {
id: window.id,
session: windowSession
});
},
close: function () {
root.command("window.close", {
id: window.id,
session: windowSession
});
}
};
}
function activateWorkspace(workspace) {
if (!seat || !workspace)
return;
command("workspace.activate", {
id: workspace.id,
session: workspace.aqueousSession
});
}
function cycleKeyboardLayout() {
if (!seat || !keyboard || !capabilities.keyboard)
return;
command("keyboard.next", {
group: keyboard.id
});
}
function toggleOverview(screenName) {
if (!capabilities.overview)
return;
command("overview.toggle", {
output: outputId(screenName || focusedOutput)
});
}
function quit() {
command("session.exit");
}
function snapshot(path) {
if (!available || path !== socketPath)
return JSON.stringify({
error: "unavailable"
});
return JSON.stringify({
schema: 1,
session: session,
sequence: sequence,
type: "snapshot",
base_sequence: null,
upsert: entities,
removed: []
});
}
IpcHandler {
target: "aqueous"
function snapshot(path: string): string {
return root.snapshot(path);
}
function status(): string {
return JSON.stringify({
available: root.available,
session: root.session,
sequence: root.sequence,
focusedOutput: root.focusedOutput,
outputs: root.outputs.length,
windows: root.windows.length,
workspaces: root.workspaces.length,
keyboardLayout: root.keyboardLayout,
overviewOutput: root.sessionState.overview_output || null,
overviewWindow: root.sessionState.overview_window || null,
error: root.state.error || ""
});
}
function overview(action: string, output: string): string {
if (!["show", "hide", "toggle"].includes(action))
return "INVALID_ACTION";
if (!root.available || root.locked || !root.capabilities.overview)
return "UNAVAILABLE";
root.command("overview." + action, {
output: root.outputId(output || root.focusedOutput)
});
return "OVERVIEW_REQUESTED";
}
function selectSeat(name: string): string {
if (name && !root.seats.some(s => s.id === name))
return "SEAT_NOT_FOUND";
root.seatName = name;
return "SEAT_SELECTED";
}
}
function overlapsDock(screenName, position, thickness, width, height) {
const output = outputs.find(o => o.name === screenName);
if (!output)
return false;
return windows.some(w => {
if (w.output !== output.id || !w.visible || w.minimized)
return false;
const box = w.outer_geometry;
const x = box.x - output.bounds.x;
const y = box.y - output.bounds.y;
switch (position) {
case SettingsData.Position.Top:
return y < thickness && y + box.height > 0;
case SettingsData.Position.Left:
return x < thickness && x + box.width > 0;
case SettingsData.Position.Right:
return x < width && x + box.width > width - thickness;
default:
return y < height && y + box.height > height - thickness;
}
});
}
Component {
id: jsonProcessComponent
Process {
id: jsonProcess
property var input: null
property var callback: null
property bool finished: false
property Timer deadline: Timer {
id: jsonDeadline
interval: 8000
onTriggered: jsonProcess.finish(null, "command completion uncertain: timeout")
}
stdinEnabled: input !== null
function finish(result, error) {
if (finished)
return;
finished = true;
jsonDeadline.stop();
if (running)
signal(9);
root.pendingProcesses = root.pendingProcesses.filter(p => p !== jsonProcess);
const complete = callback;
callback = null;
if (complete)
complete(result, error);
Qt.callLater(() => jsonProcess.destroy());
}
onStarted: {
if (input !== null) {
write(input);
stdinEnabled = false;
}
}
stdout: StdioCollector {
waitForEnd: false
onDataChanged: {
if (data.byteLength > root.maxBatchBytes + 1)
jsonProcess.finish(null, "process output exceeds 4 MiB");
}
}
stderr: SplitParser {
splitMarker: ""
}
onExited: (code, status) => {
if (finished)
return;
let result = null;
let error = "";
try {
result = root.parseJson(stdout.text);
if (code !== 0 || status !== 0)
error = result?.code ? result.code + ": " + (result.message || "") : (result?.status || "command failed (" + code + ")");
} catch (e) {
error = String(e);
}
finish(result, error);
}
onRunningChanged: {
if (!running)
Qt.callLater(() => {
if (!jsonProcess.finished)
jsonProcess.finish(null, "command could not start");
});
}
}
}
}
+3 -1
View File
@@ -135,9 +135,11 @@ Singleton {
return screens.length > 0 ? widgetRegistry[widgetId][screens[0]] : null; return screens.length > 0 ? widgetRegistry[widgetId][screens[0]] : null;
} }
readonly property bool focusedScreenDetectionSupported: CompositorService.isHyprland || CompositorService.isNiri || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle readonly property bool focusedScreenDetectionSupported: (CompositorService.isAqueous && AqueousService.available) || CompositorService.isHyprland || CompositorService.isNiri || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
function getFocusedScreenName() { function getFocusedScreenName() {
if (CompositorService.isAqueous && AqueousService.available)
return AqueousService.focusedOutput;
if (CompositorService.isHyprland && Hyprland.focusedWorkspace?.monitor) if (CompositorService.isHyprland && Hyprland.focusedWorkspace?.monitor)
return Hyprland.focusedWorkspace.monitor.name; return Hyprland.focusedWorkspace.monitor.name;
if (CompositorService.isNiri && NiriService.currentOutput) if (CompositorService.isNiri && NiriService.currentOutput)
+91 -2
View File
@@ -21,8 +21,47 @@ Singleton {
property bool isScroll: false property bool isScroll: false
property bool isMiracle: false property bool isMiracle: false
property bool isLabwc: false property bool isLabwc: false
property bool isAqueous: false
property string compositor: "unknown" property string compositor: "unknown"
property bool compositorDetected: false property bool compositorDetected: false
property bool outputPowerAvailable: false
readonly property bool genericPowerBackend: compositorDetected && !isNiri && !isHyprland && !isMango && !isSway && !isScroll && !isMiracle && !isLabwc
onGenericPowerBackendChanged: probeOutputPower()
function probeOutputPower() {
outputPowerAvailable = false;
if (!genericPowerBackend)
return;
const backend = compositor;
Proc.runCommand("output-power-probe", [Proc.dmsBin, "dpms", "list"], (output, code) => {
if (root.compositor === backend && root.genericPowerBackend)
root.outputPowerAvailable = code === 0;
}, 0, 5000);
}
function setOutputPower(on) {
Proc.runCommand("output-power-action", [Proc.dmsBin, "dpms", on ? "on" : "off"], (output, code) => {
if (code !== 0)
ToastService.showError(I18n.tr("Error"), I18n.tr("Failed to change display power", "Error shown when changing monitor power fails"));
}, 0, 12000);
}
Connections {
target: DMSService
function onConnectionStateChanged() {
if (DMSService.isConnected)
root.probeOutputPower();
else
root.outputPowerAvailable = false;
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root.probeOutputPower();
}
}
readonly property bool frameCompositorLayoutReady: (!isNiri || NiriService.frameLayoutReady) && (!isHyprland || HyprlandService.frameLayoutReady) readonly property bool frameCompositorLayoutReady: (!isNiri || NiriService.frameLayoutReady) && (!isHyprland || HyprlandService.frameLayoutReady)
readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1" readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1"
@@ -45,9 +84,19 @@ Singleton {
signal toplevelsChanged signal toplevelsChanged
readonly property bool supportsMinimize: DankCommon.Compositor.supportsMinimize readonly property bool supportsMinimize: isAqueous && AqueousService.available ? AqueousService.capabilities.commands && !AqueousService.locked : DankCommon.Compositor.supportsMinimize
Connections {
target: AqueousService
function onStateChanged() {
if (root.isAqueous)
root.scheduleSort();
}
}
function canMinimize(toplevel) { function canMinimize(toplevel) {
if (isAqueous && toplevel?.aqueousWindowId)
return AqueousService.available && !AqueousService.locked && AqueousService.capabilities.commands && toplevel.canMinimize;
return supportsMinimize && toplevel && toplevel.minimized !== undefined; return supportsMinimize && toplevel && toplevel.minimized !== undefined;
} }
@@ -59,6 +108,10 @@ Singleton {
function activateToplevel(toplevel) { function activateToplevel(toplevel) {
if (!toplevel) if (!toplevel)
return; return;
if (isAqueous && toplevel.aqueousWindowId) {
toplevel.activate();
return;
}
closeNiriOverviewOnWindowFocus(); closeNiriOverviewOnWindowFocus();
if (canMinimize(toplevel) && toplevel.minimized) if (canMinimize(toplevel) && toplevel.minimized)
toplevel.minimized = false; toplevel.minimized = false;
@@ -147,7 +200,9 @@ Singleton {
function getFocusedScreen() { function getFocusedScreen() {
let screenName = ""; let screenName = "";
if (isHyprland && Hyprland.focusedWorkspace?.monitor) if (isAqueous && AqueousService.available)
screenName = AqueousService.focusedOutput;
else if (isHyprland && Hyprland.focusedWorkspace?.monitor)
screenName = Hyprland.focusedWorkspace.monitor.name; screenName = Hyprland.focusedWorkspace.monitor.name;
else if (isNiri && NiriService.currentOutput) else if (isNiri && NiriService.currentOutput)
screenName = NiriService.currentOutput; screenName = NiriService.currentOutput;
@@ -267,6 +322,8 @@ Singleton {
} }
function computeSortedToplevels() { function computeSortedToplevels() {
if (isAqueous && AqueousService.available)
return AqueousService.toplevels;
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
return []; return [];
@@ -517,6 +574,10 @@ Singleton {
} }
function filterCurrentWorkspace(toplevels, screen) { function filterCurrentWorkspace(toplevels, screen) {
if (isAqueous && AqueousService.available) {
const active = AqueousService.workspacesForOutput(_screenName(screen)).filter(w => w.active).map(w => w.id);
return toplevels.filter(t => active.includes(t.aqueousWorkspaceId));
}
if (useNiriSorting) if (useNiriSorting)
return NiriService.filterCurrentWorkspace(toplevels, screen); return NiriService.filterCurrentWorkspace(toplevels, screen);
if (useMangoSorting) if (useMangoSorting)
@@ -528,6 +589,8 @@ Singleton {
function fullscreenToplevelOnScreen(screenOrName) { function fullscreenToplevelOnScreen(screenOrName) {
const screenName = _screenName(screenOrName); const screenName = _screenName(screenOrName);
if (isAqueous && AqueousService.available)
return AqueousService.windows.some(w => w.output === AqueousService.outputId(screenName) && w.visible && w.fullscreen);
if (!screenName || !ToplevelManager.toplevels?.values) if (!screenName || !ToplevelManager.toplevels?.values)
return false; return false;
@@ -657,6 +720,12 @@ Singleton {
return frameConfiguredForScreen(screenOrName); return frameConfiguredForScreen(screenOrName);
} }
function overviewActiveOnScreen(screenOrName) {
if (isAqueous && AqueousService.available)
return !!AqueousService.sessionState.overview_output && AqueousService.sessionState.overview_output === AqueousService.outputId(_screenName(screenOrName));
return isNiri && NiriService.inOverview;
}
function usesConnectedFrameChromeForScreen(screenOrName) { function usesConnectedFrameChromeForScreen(screenOrName) {
return FrameTransitionState.effectiveConnectedFrameModeActive && frameWindowVisibleForScreen(screenOrName); return FrameTransitionState.effectiveConnectedFrameModeActive && frameWindowVisibleForScreen(screenOrName);
} }
@@ -930,6 +999,8 @@ Singleton {
return "miracle"; return "miracle";
case "labwc": case "labwc":
return "labwc"; return "labwc";
case "aqueous":
return "aqueous";
default: default:
return ""; return "";
} }
@@ -943,6 +1014,7 @@ Singleton {
isScroll = name === "scroll"; isScroll = name === "scroll";
isMiracle = name === "miracle"; isMiracle = name === "miracle";
isLabwc = name === "labwc"; isLabwc = name === "labwc";
isAqueous = name === "aqueous";
compositor = name; compositor = name;
compositorDetected = true; compositorDetected = true;
if (isNiri) if (isNiri)
@@ -994,7 +1066,14 @@ Singleton {
// of winning on a stale env var. // of winning on a stale env var.
function _envDetectionCandidates() { function _envDetectionCandidates() {
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || ""; const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || "";
const aqueousSocket = Quickshell.env("AQUEOUS_SOCKET") || "";
return [ return [
{
name: "aqueous",
present: !!aqueousSocket,
test: ["test", "-S", aqueousSocket],
detail: "AQUEOUS_SOCKET " + aqueousSocket
},
{ {
name: "mango", name: "mango",
present: !!mangoSignature, present: !!mangoSignature,
@@ -1076,6 +1155,11 @@ Singleton {
} }
if (isLabwc) { if (isLabwc) {
Quickshell.execDetached(["dms", "dpms", "off"]); Quickshell.execDetached(["dms", "dpms", "off"]);
return;
}
if (outputPowerAvailable) {
setOutputPower(false);
return;
} }
log.warn("Cannot power off monitors, unknown compositor"); log.warn("Cannot power off monitors, unknown compositor");
} }
@@ -1095,6 +1179,11 @@ Singleton {
} }
if (isLabwc) { if (isLabwc) {
Quickshell.execDetached(["dms", "dpms", "on"]); Quickshell.execDetached(["dms", "dpms", "on"]);
return;
}
if (outputPowerAvailable) {
setOutputPower(true);
return;
} }
log.warn("Cannot power on monitors, unknown compositor"); log.warn("Cannot power on monitors, unknown compositor");
} }
+23 -3
View File
@@ -402,7 +402,16 @@ Singleton {
} }
} }
function sendRequest(method, params, callback) { Component {
id: requestTimeoutComponent
Timer {
property var requestId
repeat: false
onTriggered: root.handleResponse({id: requestId, error: "Request timed out; operation completion is uncertain"})
}
}
function sendRequest(method, params, callback, timeoutMs) {
if (!isConnected) { if (!isConnected) {
log.warn("DMSService.sendRequest: Not connected, method:", method); log.warn("DMSService.sendRequest: Not connected, method:", method);
if (callback) { if (callback) {
@@ -424,8 +433,19 @@ Singleton {
request.params = params; request.params = params;
} }
if (callback) if (callback) {
pendingRequests[id] = callback; if (timeoutMs > 0) {
const timeout = requestTimeoutComponent.createObject(root, {requestId: id, interval: timeoutMs});
pendingRequests[id] = response => {
timeout.stop();
timeout.destroy();
callback(response);
};
timeout.start();
} else {
pendingRequests[id] = callback;
}
}
if (method.startsWith("clipboard")) { if (method.startsWith("clipboard")) {
clipboardRequestIds[id] = true; clipboardRequestIds[id] = true;
+387 -7
View File
@@ -14,8 +14,10 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("KeybindsService") readonly property var log: Log.scoped("KeybindsService")
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango property bool available: CompositorService.isAqueous || CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
property string currentProvider: { property string currentProvider: {
if (CompositorService.isAqueous)
return "aqueous";
if (CompositorService.isNiri) if (CompositorService.isNiri)
return "niri"; return "niri";
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
@@ -26,6 +28,8 @@ Singleton {
} }
readonly property string cheatsheetProvider: { readonly property string cheatsheetProvider: {
if (CompositorService.isAqueous)
return "aqueous";
if (CompositorService.isNiri) if (CompositorService.isNiri)
return "niri"; return "niri";
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
@@ -66,6 +70,23 @@ Singleton {
property var displayList: [] property var displayList: []
property int _dataVersion: 0 property int _dataVersion: 0
property string _pendingSavedKey: "" property string _pendingSavedKey: ""
readonly property bool requiresBindReview: currentProvider === "aqueous"
readonly property string bindEditSession: requiresBindReview ? aqueousSession : currentProvider
readonly property bool bindMutationBusy: aqueousBusy || saving || removeProcess.running
property bool aqueousBusy: false
property bool _aqueousLoading: false
property bool _loadPending: false
property int _aqueousRequest: 0
readonly property string aqueousSession: currentProvider === "aqueous" ? AqueousService.session : ""
onCurrentProviderChanged: _pendingSavedKey = ""
onAqueousSessionChanged: {
_aqueousRequest++;
aqueousBusy = false;
_pendingSavedKey = "";
if (aqueousSession)
Qt.callLater(root.loadBinds, false);
}
readonly property var categoryOrder: Actions.getCategoryOrder() readonly property var categoryOrder: Actions.getCategoryOrder()
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
@@ -119,7 +140,7 @@ Singleton {
Connections { Connections {
target: CompositorService target: CompositorService
function onCompositorChanged() { function onCompositorChanged() {
if (!CompositorService.isNiri && !CompositorService.isMango) if (!CompositorService.isNiri && !CompositorService.isMango && !CompositorService.isAqueous)
return; return;
Qt.callLater(root.loadBinds); Qt.callLater(root.loadBinds);
} }
@@ -161,9 +182,12 @@ Singleton {
Process { Process {
id: loadProcess id: loadProcess
running: false running: false
property string provider: ""
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (loadProcess.provider !== root.currentProvider)
return;
try { try {
root._rawData = JSON.parse(text); root._rawData = JSON.parse(text);
root._processData(); root._processData();
@@ -175,6 +199,8 @@ Singleton {
} }
onExited: exitCode => { onExited: exitCode => {
if (provider !== root.currentProvider)
return;
if (exitCode !== 0) { if (exitCode !== 0) {
log.warn("Load process failed with code:", exitCode); log.warn("Load process failed with code:", exitCode);
root.loading = false; root.loading = false;
@@ -185,9 +211,13 @@ Singleton {
Process { Process {
id: saveProcess id: saveProcess
running: false running: false
property string savedKey: ""
property string provider: ""
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (saveProcess.provider !== root.currentProvider)
return;
if (!text.trim()) if (!text.trim())
return; return;
root.lastError = text.trim(); root.lastError = text.trim();
@@ -197,12 +227,20 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
root.saving = false; root.saving = false;
if (provider !== root.currentProvider) {
savedKey = "";
return;
}
if (exitCode !== 0) { if (exitCode !== 0) {
root._pendingSavedKey = "";
savedKey = "";
log.error("Save failed with code:", exitCode); log.error("Save failed with code:", exitCode);
root.bindSaveCompleted(false); root.bindSaveCompleted(false);
return; return;
} }
root.lastError = ""; root.lastError = "";
root._pendingSavedKey = savedKey;
savedKey = "";
root.bindSaveCompleted(true); root.bindSaveCompleted(true);
if (CompositorService.isMango) if (CompositorService.isMango)
MangoService.reloadConfig(); MangoService.reloadConfig();
@@ -213,9 +251,12 @@ Singleton {
Process { Process {
id: removeProcess id: removeProcess
running: false running: false
property string provider: ""
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (removeProcess.provider !== root.currentProvider)
return;
if (!text.trim()) if (!text.trim())
return; return;
root.lastError = text.trim(); root.lastError = text.trim();
@@ -224,6 +265,8 @@ Singleton {
} }
onExited: exitCode => { onExited: exitCode => {
if (provider !== root.currentProvider)
return;
if (exitCode !== 0) { if (exitCode !== 0) {
log.error("Remove failed with code:", exitCode); log.error("Remove failed with code:", exitCode);
return; return;
@@ -339,14 +382,313 @@ Singleton {
} }
function loadBinds(showLoading) { function loadBinds(showLoading) {
if (currentProvider === "aqueous") {
_loadPending = true;
if (_aqueousLoading || aqueousBusy)
return;
_loadPending = false;
_aqueousLoading = true;
loading = true;
readAqueousBinds((snapshot, error) => {
root._aqueousLoading = false;
root.loading = false;
if (snapshot) {
root.lastError = "";
root._rawData = snapshot;
root._processData();
} else {
root.lastError = error;
root._pendingSavedKey = "";
}
if (root._loadPending) {
root._loadPending = false;
Qt.callLater(root.loadBinds, false);
}
});
return;
}
if (loadProcess.running || !available) if (loadProcess.running || !available)
return; return;
const hasData = Object.keys(_allBinds).length > 0; const hasData = Object.keys(_allBinds).length > 0;
loading = showLoading !== false && !hasData; loading = showLoading !== false && !hasData;
loadProcess.command = ["dms", "keybinds", "show", currentProvider]; loadProcess.command = ["dms", "keybinds", "show", currentProvider];
loadProcess.provider = currentProvider;
loadProcess.running = true; loadProcess.running = true;
} }
function readAqueousBinds(callback) {
const session = aqueousSession;
if (!session) {
callback(null, I18n.tr("Unavailable"));
return;
}
AqueousService.runJson(["dms", "keybinds", "show", "aqueous"], null, (snapshot, error) => {
if (session !== root.aqueousSession) {
callback(null, I18n.tr("Configuration changed. Refresh to continue.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings"));
return;
}
try {
if (error)
throw new Error(error);
bindInventory(snapshot);
} catch (e) {
log.warn("Failed to read Aqueous keybindings:", e);
callback(null, AqueousService.errorMessage(String(e)));
return;
}
callback(snapshot, "");
});
}
function bindInventory(snapshot) {
if (snapshot?.provider !== "aqueous" || typeof snapshot.generation !== "string" || !snapshot.generation || !snapshot.binds || Array.isArray(snapshot.binds))
throw new Error("invalid Aqueous keybind snapshot");
if (!Array.isArray(snapshot.binds.Compositor) || !Array.isArray(snapshot.binds.Custom))
throw new Error("missing Aqueous keybind inventory");
return Object.keys(snapshot.binds).sort().map(category => {
if (!Array.isArray(snapshot.binds[category]))
throw new Error("invalid Aqueous keybind category");
return [category, snapshot.binds[category].map(bind => {
if (typeof bind.key !== "string" || typeof bind.action !== "string")
throw new Error("invalid Aqueous binding");
return [bind.key, bind.action, bind.source || ""];
})];
});
}
function bindInventoryChanges(baseline, current) {
const flatten = snapshot => bindInventory(snapshot).reduce((all, group) => all.concat(group[1].map(bind => [group[0], bind])), []);
const before = flatten(baseline);
const after = flatten(current);
const differences = [];
for (let i = 0; i < Math.max(before.length, after.length); i++) {
if (JSON.stringify(before[i]) === JSON.stringify(after[i]))
continue;
differences.push({
before: before[i]?.[1] || null,
after: after[i]?.[1] || null
});
}
return differences;
}
function bindingsForKey(snapshot, key) {
if (!key)
return [];
bindInventory(snapshot);
return Object.values(snapshot.binds).reduce((matches, category) => matches.concat(category.filter(bind => bind.key === key)), []);
}
function bindEditIssue(draft, current, reviewing) {
bindInventory(current);
if (!["set", "remove", "reset"].includes(draft.operation))
return "invalid_binding";
const original = bindingsForKey(current, draft.originalKey);
if (draft.originalKey && original.length !== 1)
return original.length ? "ambiguous_target" : "target_removed";
if (!reviewing && draft.originalKey && original[0].action !== draft.originalAction)
return "target_changed";
if (draft.operation !== "set")
return draft.originalKey ? "" : "target_removed";
const data = draft.data;
if (!data?.key || !data.action)
return "invalid_binding";
if (data.key !== draft.originalKey && bindingsForKey(current, data.key).length)
return "destination_occupied";
if (!data.action.startsWith("spawn ") && !current.binds.Compositor.some(bind => bind.action === data.action))
return "invalid_action";
return "";
}
function captureBindEdit(binding, key) {
if (!requiresBindReview)
return null;
bindInventory(_rawData);
if (!bindEditSession)
throw new Error(I18n.tr("Unavailable"));
return {
provider: currentProvider,
session: bindEditSession,
action: binding.action || "",
binding: JSON.parse(JSON.stringify(binding)),
baseline: JSON.parse(JSON.stringify(_rawData)),
originalKey: key || "",
originalAction: binding.action || "",
operation: "set",
data: {
key: key || "",
action: binding.action || "",
desc: binding.desc || ""
}
};
}
function updateBindEdit(draft, key, data, operation) {
const originals = bindingsForKey(draft.baseline, key);
return Object.assign({}, draft, {
operation: operation || draft.operation,
originalKey: key,
originalAction: originals.length ? originals[0].action : "",
data: JSON.parse(JSON.stringify(data || draft.data))
});
}
function loadBindReview(callback) {
if (requiresBindReview) {
readAqueousBinds(callback);
return;
}
callback(null, I18n.tr("Unavailable"));
}
function reconcileBindEdit(draft, snapshot) {
const issue = bindEditIssue(draft, snapshot, true);
if (issue)
return {
code: issue
};
const originals = bindingsForKey(snapshot, draft.originalKey);
return {
draft: Object.assign({}, draft, {
baseline: JSON.parse(JSON.stringify(snapshot)),
originalAction: originals.length ? originals[0].action : ""
})
};
}
function bindEditError(code) {
switch (code) {
case "external_change":
return I18n.tr("Keybindings changed. Refresh and review your edit before saving.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "target_removed":
return I18n.tr("The original shortcut was removed. Discard this edit or add a new shortcut.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "ambiguous_target":
return I18n.tr("Multiple bindings use the original shortcut. Resolve the duplicate bindings first.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "destination_occupied":
return I18n.tr("The new shortcut is already in use. Choose another shortcut.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "target_changed":
return I18n.tr("The original shortcut changed. Refresh and review your edit.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "invalidated":
return I18n.tr("The compositor session changed. Discard this edit before starting a new one.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "uncertain":
return I18n.tr("The save result is unknown. Refresh and check the current bindings.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
case "invalid_action":
return I18n.tr("The selected action is no longer available.", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings");
default:
return I18n.tr("Failed to save keybind");
}
}
function describeBindReview(draft, current) {
if (!draft || !current)
return "";
const describe = (snapshot, key) => {
const matches = bindingsForKey(snapshot, key);
return matches.length ? matches.map(bind => bind.key + " → " + bind.action).join("\n") : I18n.tr("None");
};
const original = describe(draft.baseline, draft.originalKey);
const currentBind = describe(current, draft.originalKey);
const proposed = draft.operation === "set" ? draft.data.key + " → " + draft.data.action : I18n.tr("Remove");
const destination = draft.operation === "set" ? describe(current, draft.data.key) : I18n.tr("None");
const describeChange = bind => bind ? (bind[0] || I18n.tr("Not bound")) + " → " + bind[1] : I18n.tr("None");
const changes = bindInventoryChanges(draft.baseline, current).map(change => I18n.tr("Previous: %1\nCurrent: %2", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings").arg(describeChange(change.before)).arg(describeChange(change.after))).join("\n\n");
return I18n.tr("Original: %1\nCurrent: %2\nProposed: %3\nCurrent destination: %4", "Aqueous keyboard shortcut editor, explaining a conflict or comparing an unsaved edit with current bindings").arg(original).arg(currentBind).arg(proposed).arg(destination) + (changes ? "\n\n" + changes : "");
}
function _mutateAqueous(draft, callback) {
if (aqueousBusy || saving || removeProcess.running) {
callback({
success: false,
code: "busy"
});
return;
}
if (!draft?.baseline || !aqueousSession || draft.session !== aqueousSession) {
callback({
success: false,
code: "invalidated"
});
return;
}
const edit = JSON.parse(JSON.stringify(draft));
const request = ++_aqueousRequest;
aqueousBusy = true;
const complete = result => {
if (request !== root._aqueousRequest)
return;
root.aqueousBusy = false;
callback(result);
if (result.success || root._loadPending) {
root._loadPending = false;
Qt.callLater(root.loadBinds, false);
}
};
readAqueousBinds((snapshot, error) => {
if (request !== root._aqueousRequest)
return;
if (error) {
complete({
success: false,
code: "load_failed",
message: error
});
return;
}
try {
if (JSON.stringify(bindInventory(edit.baseline)) !== JSON.stringify(bindInventory(snapshot))) {
complete({
success: false,
code: "external_change",
snapshot: snapshot
});
return;
}
const issue = bindEditIssue(edit, snapshot, false);
if (issue) {
complete({
success: false,
code: issue,
snapshot: snapshot
});
return;
}
} catch (e) {
complete({
success: false,
code: "invalid_snapshot",
message: String(e)
});
return;
}
const args = ["dms", "keybinds", edit.operation, currentProvider, edit.operation === "set" ? edit.data.key : edit.originalKey];
if (edit.operation === "set") {
args.push(edit.data.action);
if (edit.originalKey && edit.originalKey !== edit.data.key)
args.push("--replace-key", edit.originalKey);
}
args.push("--expected-generation", snapshot.generation, "--json");
AqueousService.runJson(args, null, (result, error) => {
if (request !== root._aqueousRequest)
return;
if (error || result?.success !== true || !result.generation) {
complete({
success: false,
code: result?.success === false && result.code ? result.code : "uncertain",
message: result?.message || error
});
return;
}
if (edit.operation === "set")
root._pendingSavedKey = edit.data.key;
complete(result);
if (edit.operation === "set")
root.bindSaveCompleted(true);
else
root.bindRemoved(edit.originalKey);
});
});
}
function _processData() { function _processData() {
keybinds = _rawData || {}; keybinds = _rawData || {};
modKey = currentProvider === "niri" ? (_rawData?.modKey || "Super") : "Super"; modKey = currentProvider === "niri" ? (_rawData?.modKey || "Super") : "Super";
@@ -504,7 +846,11 @@ Singleton {
return []; return [];
} }
function saveBind(originalKey, bindData) { function saveBind(originalKey, bindData, draft, callback) {
if (currentProvider === "aqueous") {
_mutateAqueous(draft, callback || (() => {}));
return;
}
if (readOnly) { if (readOnly) {
showHyprlandReadOnlyWarning(); showHyprlandReadOnlyWarning();
return; return;
@@ -512,7 +858,8 @@ Singleton {
if (!bindData.key || !Actions.isValidAction(bindData.action)) if (!bindData.key || !Actions.isValidAction(bindData.action))
return; return;
saving = true; saving = true;
const cmd = ["dms", "keybinds", "set", currentProvider, bindData.key, bindData.action, "--desc", bindData.desc || ""]; const cmd = ["dms", "keybinds", "set", currentProvider, bindData.key, bindData.action];
cmd.push("--desc", bindData.desc || "");
if (originalKey && originalKey !== bindData.key) if (originalKey && originalKey !== bindData.key)
cmd.push("--replace-key", originalKey); cmd.push("--replace-key", originalKey);
if (bindData.cooldownMs > 0) if (bindData.cooldownMs > 0)
@@ -526,8 +873,9 @@ Singleton {
if (bindData.flags) if (bindData.flags)
cmd.push("--flags", bindData.flags); cmd.push("--flags", bindData.flags);
saveProcess.command = cmd; saveProcess.command = cmd;
saveProcess.provider = currentProvider;
saveProcess.savedKey = bindData.key;
saveProcess.running = true; saveProcess.running = true;
_pendingSavedKey = bindData.key;
} }
property bool _hyprlandLegacyWarnShown: false property bool _hyprlandLegacyWarnShown: false
@@ -552,7 +900,11 @@ Singleton {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "hyprland-migration"); ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "hyprland-migration");
} }
function removeBind(key) { function removeBind(key, draft, callback) {
if (currentProvider === "aqueous") {
_mutateAqueous(draft, callback || (() => {}));
return;
}
if (readOnly) { if (readOnly) {
showHyprlandReadOnlyWarning(); showHyprlandReadOnlyWarning();
return; return;
@@ -560,11 +912,16 @@ Singleton {
if (!key) if (!key)
return; return;
removeProcess.command = ["dms", "keybinds", "remove", currentProvider, key]; removeProcess.command = ["dms", "keybinds", "remove", currentProvider, key];
removeProcess.provider = currentProvider;
removeProcess.running = true; removeProcess.running = true;
bindRemoved(key); bindRemoved(key);
} }
function resetBind(key) { function resetBind(key, draft, callback) {
if (currentProvider === "aqueous") {
_mutateAqueous(draft, callback || (() => {}));
return;
}
if (readOnly) { if (readOnly) {
showHyprlandReadOnlyWarning(); showHyprlandReadOnlyWarning();
return; return;
@@ -572,19 +929,42 @@ Singleton {
if (!key) if (!key)
return; return;
removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key]; removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key];
removeProcess.provider = currentProvider;
removeProcess.running = true; removeProcess.running = true;
bindRemoved(key); bindRemoved(key);
} }
function getActionLabel(action) { function getActionLabel(action) {
if (currentProvider === "aqueous")
return (_rawData?.binds?.Compositor || []).find(b => b.action === action)?.desc || Actions.getActionLabel(action, currentProvider);
return Actions.getActionLabel(action, currentProvider); return Actions.getActionLabel(action, currentProvider);
} }
function isKnownCompositorAction(action) {
if (currentProvider === "aqueous")
return (_rawData?.binds?.Compositor || []).some(b => b.action === action);
return Actions.isKnownCompositorAction(currentProvider, action);
}
function getCompositorCategories() { function getCompositorCategories() {
if (currentProvider === "aqueous")
return ["Compositor"];
return Actions.getCompositorCategories(currentProvider); return Actions.getCompositorCategories(currentProvider);
} }
function getCompositorActions(category) { function getCompositorActions(category) {
if (currentProvider === "aqueous") {
const seen = new Set();
return (_rawData?.binds?.Compositor || []).filter(b => {
if (seen.has(b.action))
return false;
seen.add(b.action);
return true;
}).map(b => ({
id: b.action,
label: b.desc
}));
}
return Actions.getCompositorActions(currentProvider, category); return Actions.getCompositorActions(currentProvider, category);
} }
+12 -4
View File
@@ -176,20 +176,20 @@ Singleton {
Process { Process {
id: uwsmLogout id: uwsmLogout
property bool notRunning: false
command: ["uwsm", "stop"] command: ["uwsm", "stop"]
running: false running: false
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: data => { onRead: data => {
if (data.trim().toLowerCase().includes("not running")) { if (data.trim().toLowerCase().includes("not running"))
_logout(); uwsmLogout.notRunning = true;
}
} }
} }
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode === 0) { if (exitCode === 0 && !notRunning) {
return; return;
} }
_logout(); _logout();
@@ -393,13 +393,21 @@ Singleton {
// * Session management // * Session management
function logout() { function logout() {
if (hasUwsm) { if (hasUwsm) {
if (uwsmLogout.running)
return;
uwsmLogout.notRunning = false;
uwsmLogout.running = true; uwsmLogout.running = true;
return;
} }
_logout(); _logout();
} }
function _logout() { function _logout() {
if (SettingsData.customPowerActionLogout.length === 0) { if (SettingsData.customPowerActionLogout.length === 0) {
if (CompositorService.isAqueous) {
AqueousService.quit();
return;
}
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.quit(); NiriService.quit();
return; return;
@@ -44,6 +44,10 @@ Singleton {
"isNiri": () => CompositorService.isNiri, "isNiri": () => CompositorService.isNiri,
"isHyprland": () => CompositorService.isHyprland, "isHyprland": () => CompositorService.isHyprland,
"isMango": () => CompositorService.isMango, "isMango": () => CompositorService.isMango,
"isAqueous": () => CompositorService.isAqueous,
"nativeOverviewCapable": () => CompositorService.isNiri || CompositorService.isAqueous,
"smartDockCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous,
"workspaceFollowFocusCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isAqueous,
"isHyprlandOrNiri": () => CompositorService.isHyprland || CompositorService.isNiri, "isHyprlandOrNiri": () => CompositorService.isHyprland || CompositorService.isNiri,
"windowRulesCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango, "windowRulesCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
"layoutCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango, "layoutCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
+27 -14
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common
import qs.Services import qs.Services
Singleton { Singleton {
@@ -109,7 +110,7 @@ Singleton {
function applyConfiguration(heads, callback) { function applyConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) { if (callback) {
callback(false, "Not connected"); callback(false, I18n.tr("Not connected"));
} }
return; return;
} }
@@ -122,11 +123,11 @@ Singleton {
DMSService.sendRequest("wlroutput.applyConfiguration", { DMSService.sendRequest("wlroutput.applyConfiguration", {
"heads": heads "heads": heads
}, response => { }, response => {
const success = !response.error; const success = !response.error && response.result?.success === true;
const message = response.error || response.result?.message || ""; const message = response.error || response.result?.message || "";
if (response.error) { if (!success) {
log.warn("applyConfiguration error:", response.error); log.warn("applyConfiguration error:", message);
} else { } else {
log.debug("Configuration applied successfully"); log.debug("Configuration applied successfully");
} }
@@ -135,13 +136,13 @@ Singleton {
if (callback) { if (callback) {
callback(success, message); callback(success, message);
} }
}); }, 5000);
} }
function testConfiguration(heads, callback) { function testConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) { if (callback) {
callback(false, "Not connected"); callback(false, I18n.tr("Not connected"));
} }
return; return;
} }
@@ -151,11 +152,11 @@ Singleton {
DMSService.sendRequest("wlroutput.testConfiguration", { DMSService.sendRequest("wlroutput.testConfiguration", {
"heads": heads "heads": heads
}, response => { }, response => {
const success = !response.error; const success = !response.error && response.result?.success === true;
const message = response.error || response.result?.message || ""; const message = response.error || response.result?.message || "";
if (response.error) { if (!success) {
log.warn("testConfiguration error:", response.error); log.warn("testConfiguration error:", message);
} else { } else {
log.debug("Configuration test passed"); log.debug("Configuration test passed");
} }
@@ -163,7 +164,7 @@ Singleton {
if (callback) { if (callback) {
callback(success, message); callback(success, message);
} }
}); }, 5000);
} }
function setOutputEnabled(outputName, enabled, callback) { function setOutputEnabled(outputName, enabled, callback) {
@@ -282,9 +283,22 @@ Singleton {
// NiriService, HyprlandService and MangoService. Instead of writing a // NiriService, HyprlandService and MangoService. Instead of writing a
// config file, the changes are applied directly via the // config file, the changes are applied directly via the
// wlr-output-management protocol. // wlr-output-management protocol.
function applyOutputsConfig(outputsData, connectedOutputs) { function applyOutputsConfig(outputsData, connectedOutputs, callback) {
if (!wlrOutputAvailable) if (!wlrOutputAvailable) {
if (callback)
callback(false, I18n.tr("Not connected"));
return; return;
}
const heads = outputsConfigHeads(outputsData, connectedOutputs);
if (heads.length === 0) {
if (callback)
callback(false, I18n.tr("No monitors"));
return;
}
applyConfiguration(heads, callback);
}
function outputsConfigHeads(outputsData, connectedOutputs) {
const heads = []; const heads = [];
for (const name in outputsData) { for (const name in outputsData) {
if (!connectedOutputs[name]) if (!connectedOutputs[name])
@@ -319,8 +333,7 @@ Singleton {
heads.push(head); heads.push(head);
} }
if (heads.length > 0) return heads;
applyConfiguration(heads);
} }
function transformFromName(name) { function transformFromName(name) {
+3 -1
View File
@@ -20,7 +20,7 @@ Item {
property string customPath: "" property string customPath: ""
property bool fallbackToApps: false property bool fallbackToApps: false
readonly property bool compositorAvailable: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc readonly property bool compositorAvailable: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc || CompositorService.isAqueous
readonly property bool colorize: colorOverride !== "" readonly property bool colorize: colorOverride !== ""
readonly property string resolvedMode: { readonly property string resolvedMode: {
const fallback = fallbackToApps ? "apps" : ""; const fallback = fallbackToApps ? "apps" : "";
@@ -53,6 +53,8 @@ Item {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg"; return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
case "labwc": case "labwc":
return "file://" + Theme.shellDir + "/assets/labwc.png"; return "file://" + Theme.shellDir + "/assets/labwc.png";
case "aqueous":
return "file://" + Theme.shellDir + "/assets/aqueous.svg";
default: default:
return ""; return "";
} }
+1 -1
View File
@@ -73,7 +73,7 @@ Item {
onClipHeightChanged: settleKickAction.restart() onClipHeightChanged: settleKickAction.restart()
function _runSettleKick() { function _runSettleKick() {
if (!targetWindow?.visible) if (!BlurService.compositorSupported || !targetWindow?.visible)
return; return;
kick(); kick();
settleRepeatTimer.restart(); settleRepeatTimer.restart();
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
<title id="title">Aqueous logo</title>
<desc id="desc">Vector tracing of the supplied logo, with a transparent background and editable cubic Bézier paths.</desc>
<g id="shadow" fill="#000000" fill-rule="evenodd" transform="translate(10 10)">
<path id="shadow-outer-body" d="M493.00,977.93 C457.96,976.49 421.00,970.80 393.00,962.55 C390.52,961.82 385.80,960.44 382.50,959.48 C357.36,952.19 321.40,937.66 299.00,925.73 C233.85,891.05 182.87,848.02 139.98,791.50 C97.33,735.30 66.02,664.91 53.08,596.09 C47.64,567.20 45.76,546.76 45.64,515.50 C45.48,473.64 48.70,446.26 58.63,405.07 C62.34,389.65 71.56,360.03 75.93,349.50 C76.38,348.40 78.68,342.77 81.03,337.00 C98.01,295.28 123.33,253.68 155.91,214.00 C165.42,202.41 198.02,169.20 209.90,159.00 C218.94,151.24 234.76,138.97 245.96,131.03 C257.06,123.17 272.68,113.00 273.66,113.00 C274.04,113.00 275.05,112.38 275.92,111.62 C278.29,109.54 300.02,97.61 312.50,91.54 C327.18,84.39 354.88,73.43 374.27,67.09 C391.15,61.57 416.83,55.06 431.50,52.58 C458.67,47.99 491.60,45.00 515.06,45.00 C549.18,45.00 599.52,52.73 632.75,63.08 C644.18,66.63 663.45,73.28 664.50,74.03 C665.05,74.42 667.30,75.32 669.50,76.03 C676.79,78.37 705.75,91.88 716.50,97.96 C725.91,103.27 747.69,116.45 749.88,118.15 C751.20,119.17 753.31,120.00 754.57,120.00 C755.85,120.00 762.08,116.32 768.54,111.75 C774.95,107.21 782.80,101.70 785.99,99.50 C789.17,97.30 793.74,94.13 796.14,92.44 C833.80,66.06 845.08,60.53 866.72,57.85 C878.08,56.44 912.19,57.37 926.00,59.48 C954.85,63.87 960.98,67.63 969.98,86.50 C972.66,92.13 975.57,100.21 976.62,105.00 C978.44,113.22 978.50,124.14 978.50,431.23 L978.50,748.96 976.07,750.73 C971.68,753.93 938.26,777.32 925.75,785.96 C913.27,794.57 862.56,829.90 813.00,864.52 C756.41,904.05 734.46,917.79 703.50,933.05 C649.55,959.65 591.05,975.26 536.77,977.53 C530.02,977.81 521.35,978.18 517.50,978.35 C513.65,978.52 502.62,978.33 493.00,977.93 Z M540.50,760.91 C564.52,759.01 587.54,754.04 605.50,746.89 C607.15,746.23 608.95,745.58 609.50,745.44 C616.94,743.53 664.48,720.82 686.65,708.59 C706.72,697.51 739.96,676.85 756.31,665.29 C792.57,639.65 813.12,622.72 837.58,598.35 C867.94,568.09 879.85,550.06 884.01,528.00 C885.10,522.27 885.38,505.41 884.50,498.50 C882.34,481.39 874.28,466.79 854.70,444.50 C846.40,435.05 825.95,414.73 816.95,406.98 C813.45,403.96 808.55,399.67 806.05,397.43 C785.74,379.25 741.50,347.38 711.00,328.97 C683.30,312.25 650.82,295.31 627.93,285.65 C552.68,253.90 471.81,254.04 403.31,286.05 C359.54,306.50 322.34,338.64 295.72,379.00 C276.72,407.80 265.02,437.20 258.91,471.50 C256.22,486.57 255.20,519.62 256.92,536.22 C264.36,608.31 303.55,672.65 366.50,716.14 C374.88,721.92 389.96,730.59 401.00,735.97 C429.02,749.61 459.03,757.91 491.50,760.99 C510.84,762.83 516.31,762.82 540.50,760.91 Z"/>
<path id="shadow-red-accent" d="M865.55,965.86 C850.54,963.93 836.04,957.81 818.82,946.14 C793.89,929.24 782.66,920.90 783.69,920.06 C785.84,918.29 793.34,913.04 814.00,898.84 C826.38,890.33 838.98,881.62 842.00,879.48 C845.02,877.34 858.30,868.11 871.50,858.96 C884.70,849.81 909.23,832.73 926.00,820.99 C942.77,809.26 961.59,796.14 967.80,791.85 L979.11,784.04 978.76,848.77 C978.42,912.30 978.37,913.65 976.18,921.32 C970.01,942.84 959.84,954.60 943.09,959.56 C921.81,965.88 887.45,968.66 865.55,965.86 Z"/>
<path id="shadow-inner-drop" d="M499.00,733.54 C498.18,733.34 492.77,732.64 487.00,731.99 C481.23,731.35 471.02,729.49 464.33,727.86 C393.49,710.66 336.46,665.87 306.03,603.56 C294.75,580.46 288.67,560.15 285.43,534.78 C282.39,510.98 284.76,480.72 291.49,457.61 C309.58,395.42 351.19,345.51 409.79,315.70 C437.33,301.69 459.41,294.91 488.49,291.52 C538.75,285.65 588.07,296.50 646.50,326.28 C717.65,362.54 792.04,417.06 830.89,461.42 C843.82,476.19 846.19,479.34 850.93,488.13 C854.26,494.31 855.16,497.22 856.14,505.05 C857.17,513.21 857.10,515.52 855.65,522.02 C852.00,538.35 842.61,551.59 815.59,578.54 C777.66,616.36 737.65,645.82 676.00,681.30 C659.97,690.52 624.97,708.00 608.55,714.97 C589.97,722.86 570.07,728.63 551.28,731.57 C542.46,732.95 502.69,734.45 499.00,733.54 Z"/>
</g>
<path id="outer-body" fill="#11b5c1" fill-rule="evenodd" d="M493.00,977.93 C457.96,976.49 421.00,970.80 393.00,962.55 C390.52,961.82 385.80,960.44 382.50,959.48 C357.36,952.19 321.40,937.66 299.00,925.73 C233.85,891.05 182.87,848.02 139.98,791.50 C97.33,735.30 66.02,664.91 53.08,596.09 C47.64,567.20 45.76,546.76 45.64,515.50 C45.48,473.64 48.70,446.26 58.63,405.07 C62.34,389.65 71.56,360.03 75.93,349.50 C76.38,348.40 78.68,342.77 81.03,337.00 C98.01,295.28 123.33,253.68 155.91,214.00 C165.42,202.41 198.02,169.20 209.90,159.00 C218.94,151.24 234.76,138.97 245.96,131.03 C257.06,123.17 272.68,113.00 273.66,113.00 C274.04,113.00 275.05,112.38 275.92,111.62 C278.29,109.54 300.02,97.61 312.50,91.54 C327.18,84.39 354.88,73.43 374.27,67.09 C391.15,61.57 416.83,55.06 431.50,52.58 C458.67,47.99 491.60,45.00 515.06,45.00 C549.18,45.00 599.52,52.73 632.75,63.08 C644.18,66.63 663.45,73.28 664.50,74.03 C665.05,74.42 667.30,75.32 669.50,76.03 C676.79,78.37 705.75,91.88 716.50,97.96 C725.91,103.27 747.69,116.45 749.88,118.15 C751.20,119.17 753.31,120.00 754.57,120.00 C755.85,120.00 762.08,116.32 768.54,111.75 C774.95,107.21 782.80,101.70 785.99,99.50 C789.17,97.30 793.74,94.13 796.14,92.44 C833.80,66.06 845.08,60.53 866.72,57.85 C878.08,56.44 912.19,57.37 926.00,59.48 C954.85,63.87 960.98,67.63 969.98,86.50 C972.66,92.13 975.57,100.21 976.62,105.00 C978.44,113.22 978.50,124.14 978.50,431.23 L978.50,748.96 976.07,750.73 C971.68,753.93 938.26,777.32 925.75,785.96 C913.27,794.57 862.56,829.90 813.00,864.52 C756.41,904.05 734.46,917.79 703.50,933.05 C649.55,959.65 591.05,975.26 536.77,977.53 C530.02,977.81 521.35,978.18 517.50,978.35 C513.65,978.52 502.62,978.33 493.00,977.93 Z M540.50,760.91 C564.52,759.01 587.54,754.04 605.50,746.89 C607.15,746.23 608.95,745.58 609.50,745.44 C616.94,743.53 664.48,720.82 686.65,708.59 C706.72,697.51 739.96,676.85 756.31,665.29 C792.57,639.65 813.12,622.72 837.58,598.35 C867.94,568.09 879.85,550.06 884.01,528.00 C885.10,522.27 885.38,505.41 884.50,498.50 C882.34,481.39 874.28,466.79 854.70,444.50 C846.40,435.05 825.95,414.73 816.95,406.98 C813.45,403.96 808.55,399.67 806.05,397.43 C785.74,379.25 741.50,347.38 711.00,328.97 C683.30,312.25 650.82,295.31 627.93,285.65 C552.68,253.90 471.81,254.04 403.31,286.05 C359.54,306.50 322.34,338.64 295.72,379.00 C276.72,407.80 265.02,437.20 258.91,471.50 C256.22,486.57 255.20,519.62 256.92,536.22 C264.36,608.31 303.55,672.65 366.50,716.14 C374.88,721.92 389.96,730.59 401.00,735.97 C429.02,749.61 459.03,757.91 491.50,760.99 C510.84,762.83 516.31,762.82 540.50,760.91 Z"/>
<path id="red-accent" fill="#ee4748" fill-rule="evenodd" d="M865.55,965.86 C850.54,963.93 836.04,957.81 818.82,946.14 C793.89,929.24 782.66,920.90 783.69,920.06 C785.84,918.29 793.34,913.04 814.00,898.84 C826.38,890.33 838.98,881.62 842.00,879.48 C845.02,877.34 858.30,868.11 871.50,858.96 C884.70,849.81 909.23,832.73 926.00,820.99 C942.77,809.26 961.59,796.14 967.80,791.85 L979.11,784.04 978.76,848.77 C978.42,912.30 978.37,913.65 976.18,921.32 C970.01,942.84 959.84,954.60 943.09,959.56 C921.81,965.88 887.45,968.66 865.55,965.86 Z"/>
<path id="inner-drop" fill="#0c8693" fill-rule="evenodd" d="M499.00,733.54 C498.18,733.34 492.77,732.64 487.00,731.99 C481.23,731.35 471.02,729.49 464.33,727.86 C393.49,710.66 336.46,665.87 306.03,603.56 C294.75,580.46 288.67,560.15 285.43,534.78 C282.39,510.98 284.76,480.72 291.49,457.61 C309.58,395.42 351.19,345.51 409.79,315.70 C437.33,301.69 459.41,294.91 488.49,291.52 C538.75,285.65 588.07,296.50 646.50,326.28 C717.65,362.54 792.04,417.06 830.89,461.42 C843.82,476.19 846.19,479.34 850.93,488.13 C854.26,494.31 855.16,497.22 856.14,505.05 C857.17,513.21 857.10,515.52 855.65,522.02 C852.00,538.35 842.61,551.59 815.59,578.54 C777.66,616.36 737.65,645.82 676.00,681.30 C659.97,690.52 624.97,708.00 608.55,714.97 C589.97,722.86 570.07,728.63 551.28,731.57 C542.46,732.95 502.69,734.45 499.00,733.54 Z"/>
<path id="highlight-large" fill="#2ab9c2" fill-rule="evenodd" d="M387.84,425.15 C380.29,422.64 376.22,420.02 370.57,413.98 C355.62,397.99 356.10,373.80 371.67,358.20 C391.22,338.63 423.74,344.45 436.20,369.76 C439.27,375.99 439.50,377.16 439.50,386.53 C439.50,396.14 439.33,396.94 435.77,404.16 C431.28,413.23 425.45,418.96 416.39,423.20 C407.88,427.18 396.35,427.97 387.84,425.15 Z"/>
<path id="highlight-small" fill="#2ab9c2" fill-rule="evenodd" d="M436.49,352.19 C431.29,349.76 426.28,343.99 425.43,339.47 C424.41,334.02 425.49,326.16 427.68,323.23 C428.75,321.79 431.30,319.26 433.35,317.60 C436.41,315.13 438.19,314.52 443.29,314.22 C448.39,313.91 450.30,314.27 454.00,316.25 C456.48,317.57 459.84,320.56 461.48,322.89 C464.01,326.47 464.47,328.07 464.48,333.31 C464.50,341.05 461.92,346.37 456.17,350.44 C450.77,354.25 442.47,354.99 436.49,352.19 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

@@ -407,7 +407,16 @@ def find_settings_components(content, filename, wrappers):
visible_raw = extract_property(block, "visible") visible_raw = extract_property(block, "visible")
condition_key = FILE_CONDITION_MAP.get(filename) condition_key = FILE_CONDITION_MAP.get(filename)
if visible_raw: if visible_raw:
if all(c in visible_raw for c in ("CompositorService.isNiri", "CompositorService.isHyprland", "CompositorService.isMango")): if filename == "WorkspacesTab.qml" and setting_key == "workspaceFollowFocus":
condition_key = "workspaceFollowFocusCapable"
elif "CompositorService.isAqueous" in visible_raw:
if "CompositorService.isHyprland" in visible_raw:
condition_key = "smartDockCapable"
elif "CompositorService.isNiri" in visible_raw:
condition_key = "nativeOverviewCapable"
else:
condition_key = "isAqueous"
elif all(c in visible_raw for c in ("CompositorService.isNiri", "CompositorService.isHyprland", "CompositorService.isMango")):
condition_key = "windowRulesCapable" condition_key = "windowRulesCapable"
elif "CompositorService.isNiri" in visible_raw: elif "CompositorService.isNiri" in visible_raw:
condition_key = "isNiri" condition_key = "isNiri"
@@ -1240,7 +1240,6 @@
"category": "Bar", "category": "Bar",
"keywords": [ "keywords": [
"bar", "bar",
"during",
"frame", "frame",
"niri", "niri",
"overview", "overview",
@@ -1249,8 +1248,8 @@
"statusbar", "statusbar",
"topbar" "topbar"
], ],
"description": "Show during Niri overview", "description": "Show on Overview",
"conditionKey": "isNiri" "conditionKey": "nativeOverviewCapable"
}, },
{ {
"section": "barSize", "section": "barSize",
@@ -1463,7 +1462,7 @@
"workspaces" "workspaces"
], ],
"description": "Show workspaces of the currently focused monitor", "description": "Show workspaces of the currently focused monitor",
"conditionKey": "windowRulesCapable" "conditionKey": "workspaceFollowFocusCapable"
}, },
{ {
"section": "workspaceSettings", "section": "workspaceSettings",
@@ -1488,7 +1487,7 @@
], ],
"icon": "view_module", "icon": "view_module",
"description": "Show workspace index numbers in the top bar workspace switcher", "description": "Show workspace index numbers in the top bar workspace switcher",
"conditionKey": "windowRulesCapable" "conditionKey": "smartDockCapable"
}, },
{ {
"section": "groupActiveWorkspaceApps", "section": "groupActiveWorkspaceApps",
@@ -1611,7 +1610,7 @@
"workspace" "workspace"
], ],
"description": "Reverse workspace switch direction when scrolling over the bar", "description": "Reverse workspace switch direction when scrolling over the bar",
"conditionKey": "windowRulesCapable" "conditionKey": "smartDockCapable"
}, },
{ {
"section": "workspaceUnfocusedMonitorSeparateAppearance", "section": "workspaceUnfocusedMonitorSeparateAppearance",
@@ -1696,7 +1695,7 @@
"workspaces" "workspaces"
], ],
"description": "Display only workspaces that contain windows", "description": "Display only workspaces that contain windows",
"conditionKey": "windowRulesCapable" "conditionKey": "smartDockCapable"
}, },
{ {
"section": "showWorkspaceApps", "section": "showWorkspaceApps",
@@ -1725,7 +1724,7 @@
"workspace" "workspace"
], ],
"description": "Display application icons in workspace indicators", "description": "Display application icons in workspace indicators",
"conditionKey": "windowRulesCapable" "conditionKey": "smartDockCapable"
}, },
{ {
"section": "showWorkspaceIndex", "section": "showWorkspaceIndex",
@@ -2015,7 +2014,7 @@
"windows" "windows"
], ],
"description": "Show dock when floating windows don", "description": "Show dock when floating windows don",
"conditionKey": "windowRulesCapable" "conditionKey": "smartDockCapable"
}, },
{ {
"section": "dockIsolateDisplays", "section": "dockIsolateDisplays",
@@ -2311,17 +2310,15 @@
"tabIndex": 5, "tabIndex": 5,
"category": "Dock", "category": "Dock",
"keywords": [ "keywords": [
"always",
"dock", "dock",
"launcher bar", "launcher bar",
"niri", "niri",
"overview", "overview",
"panel",
"show", "show",
"taskbar" "taskbar"
], ],
"description": "Always show the dock when niri", "description": "Show on Overview",
"conditionKey": "isNiri" "conditionKey": "nativeOverviewCapable"
}, },
{ {
"section": "dockLauncherLogoSizeOffset", "section": "dockLauncherLogoSizeOffset",
@@ -3615,6 +3612,23 @@
"icon": "apps", "icon": "apps",
"description": "Sync dark mode with settings portals for system-wide theme hints" "description": "Sync dark mode with settings portals for system-wide theme hints"
}, },
{
"section": "aqueousCursor",
"label": "Aqueous cursor",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
"appearance",
"aqueous",
"colors",
"cursor",
"look",
"scheme",
"style",
"theme"
],
"conditionKey": "isAqueous"
},
{ {
"section": "matugenSmartMode", "section": "matugenSmartMode",
"label": "Auto From Wallpaper", "label": "Auto From Wallpaper",
@@ -6254,6 +6268,24 @@
"icon": "animation", "icon": "animation",
"description": "Globally scale animation durations with geometric motion & spring based physics with bezier curves" "description": "Globally scale animation durations with geometric motion & spring based physics with bezier curves"
}, },
{
"section": "aqueousTypography",
"label": "Aqueous typography",
"tabIndex": 14,
"category": "Typography & Motion",
"keywords": [
"animation",
"appearance",
"aqueous",
"font",
"fonts",
"motion",
"text",
"typeface",
"typography"
],
"conditionKey": "isAqueous"
},
{ {
"section": "modalCustomAnimationDuration", "section": "modalCustomAnimationDuration",
"label": "Custom Duration", "label": "Custom Duration",
@@ -9962,7 +9994,6 @@
"keywords": [ "keywords": [
"border", "border",
"decoration", "decoration",
"during",
"frame", "frame",
"hide", "hide",
"integrations", "integrations",
@@ -9972,8 +10003,8 @@
"window" "window"
], ],
"icon": "toolbar", "icon": "toolbar",
"description": "Show during Niri overview", "description": "Show on Overview",
"conditionKey": "isNiri" "conditionKey": "nativeOverviewCapable"
}, },
{ {
"section": "frameLauncherEmergeSide", "section": "frameLauncherEmergeSide",
@@ -10056,7 +10087,6 @@
"keywords": [ "keywords": [
"border", "border",
"decoration", "decoration",
"during",
"frame", "frame",
"hide", "hide",
"niri", "niri",
@@ -10064,7 +10094,7 @@
"show", "show",
"window" "window"
], ],
"description": "Show during Niri overview", "description": "Show on Overview",
"conditionKey": "frameEnabled" "conditionKey": "frameEnabled"
}, },
{ {