mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-22 03:04:01 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84bb4d4753 | ||
|
|
f5acb716b9 | ||
|
|
b138ec1b0f | ||
|
|
5a72cd9616 | ||
|
|
ce1ecfc6bc | ||
|
|
80472c4db8 | ||
|
|
49513047b2 | ||
|
|
045c5c3a89 | ||
|
|
398b360c48 | ||
|
|
a39196e992 | ||
|
|
2ac1fe670c | ||
|
|
f39529d58c | ||
|
|
8874a804b6 |
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -46,7 +47,7 @@ var keybindsSetCmd = &cobra.Command{
|
||||
Short: "Set a keybind override",
|
||||
Long: "Create or update a keybind override for the specified provider",
|
||||
Args: cobra.ExactArgs(3),
|
||||
Run: runKeybindsSet,
|
||||
Run: runKeybindsEdit,
|
||||
}
|
||||
|
||||
var keybindsRemoveCmd = &cobra.Command{
|
||||
@@ -54,7 +55,7 @@ var keybindsRemoveCmd = &cobra.Command{
|
||||
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.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runKeybindsRemove,
|
||||
Run: runKeybindsEdit,
|
||||
}
|
||||
|
||||
var keybindsResetCmd = &cobra.Command{
|
||||
@@ -62,10 +63,14 @@ var keybindsResetCmd = &cobra.Command{
|
||||
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.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runKeybindsReset,
|
||||
Run: runKeybindsEdit,
|
||||
}
|
||||
|
||||
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")
|
||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||
@@ -93,6 +98,9 @@ func initializeProviders() {
|
||||
registry := keybinds.GetDefaultRegistry()
|
||||
|
||||
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 {
|
||||
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||
}
|
||||
@@ -206,85 +214,92 @@ func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||
printCheatSheet(provider)
|
||||
}
|
||||
|
||||
func getWritableProvider(name string) keybinds.WritableProvider {
|
||||
provider, err := keybinds.GetDefaultRegistry().Get(name)
|
||||
func keybindEditFailure(err error) map[string]any {
|
||||
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 {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||
providerName, key, action := args[0], args[1], args[2]
|
||||
writable := getWritableProvider(providerName)
|
||||
|
||||
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||
_ = writable.RemoveBind(replaceKey)
|
||||
}
|
||||
|
||||
options := make(map[string]any)
|
||||
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||
options["allow-when-locked"] = true
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||
options["cooldown-ms"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||
options["repeat"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||
options["allow-inhibiting"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
|
||||
desc, _ := cmd.Flags().GetString("desc")
|
||||
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||
log.Fatalf("Error setting keybind: %v", err)
|
||||
}
|
||||
|
||||
output, _ := json.MarshalIndent(map[string]any{
|
||||
"success": true,
|
||||
"key": key,
|
||||
"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))
|
||||
result := map[string]any{"success": true, "key": key}
|
||||
switch cmd.Name() {
|
||||
case "set":
|
||||
options := make(map[string]any)
|
||||
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||
options["allow-when-locked"] = true
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||
options["cooldown-ms"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||
options["repeat"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||
options["allow-inhibiting"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||
_ = writable.RemoveBind(replaceKey)
|
||||
}
|
||||
desc, _ := cmd.Flags().GetString("desc")
|
||||
err = writable.SetBind(key, args[2], desc, options)
|
||||
result["action"] = args[2]
|
||||
result["path"] = writable.GetOverridePath()
|
||||
case "remove":
|
||||
err = writable.RemoveBind(key)
|
||||
result["removed"] = true
|
||||
case "reset":
|
||||
err = writable.ResetBind(key)
|
||||
result["reset"] = true
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported keybind operation: %s", cmd.Name())
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
var (
|
||||
ssOutputName string
|
||||
ssSeat string
|
||||
ssCursor string
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
@@ -58,7 +59,7 @@ Modes:
|
||||
full - Capture the focused output
|
||||
all - Capture all outputs combined
|
||||
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
|
||||
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 all # All screens combined
|
||||
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 --reset # Reset last region pre-selection
|
||||
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{
|
||||
Use: "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,
|
||||
}
|
||||
|
||||
@@ -165,6 +166,7 @@ var notifyActionCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
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().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||
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.Mode = mode
|
||||
config.OutputName = ssOutputName
|
||||
config.Seat = ssSeat
|
||||
if strings.EqualFold(ssCursor, "on") {
|
||||
config.Cursor = screenshot.CursorOn
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ type DMSBindsStatus struct {
|
||||
}
|
||||
|
||||
type CheatSheet struct {
|
||||
Generation string `json:"generation,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Provider string `json:"provider"`
|
||||
ModKey string `json:"modKey,omitempty"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -21,6 +22,7 @@ const (
|
||||
CompositorScroll
|
||||
CompositorMiracle
|
||||
CompositorMango
|
||||
CompositorAqueous
|
||||
)
|
||||
|
||||
var detectedCompositor Compositor = -1
|
||||
@@ -35,6 +37,7 @@ func DetectCompositor() Compositor {
|
||||
needsStat bool
|
||||
compositor Compositor
|
||||
}{
|
||||
{os.Getenv("AQUEOUS_SOCKET"), true, CompositorAqueous},
|
||||
{os.Getenv("MANGO_INSTANCE_SIGNATURE"), true, CompositorMango},
|
||||
{os.Getenv("NIRI_SOCKET"), true, CompositorNiri},
|
||||
{os.Getenv("SCROLLSOCK"), true, CompositorScroll},
|
||||
@@ -78,8 +81,14 @@ func GetActiveWindow() (*WindowGeometry, error) {
|
||||
return getHyprlandActiveWindow()
|
||||
case CompositorMango:
|
||||
return getMangoActiveWindow()
|
||||
case CompositorAqueous:
|
||||
model, err := aqueousSnapshot(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aqueousWindowGeometry(model, "")
|
||||
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 {
|
||||
switch DetectCompositor() {
|
||||
case CompositorAqueous:
|
||||
name, _ := aqueousFocusedOutput("")
|
||||
return name
|
||||
case CompositorHyprland:
|
||||
return getHyprlandFocusedMonitor()
|
||||
case CompositorSway:
|
||||
|
||||
@@ -220,6 +220,9 @@ func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
|
||||
if DetectCompositor() == CompositorAqueous {
|
||||
return s.captureAqueousWindow()
|
||||
}
|
||||
if DetectCompositor() == CompositorNiri {
|
||||
if s.config.Geometry {
|
||||
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) {
|
||||
if DetectCompositor() == CompositorAqueous {
|
||||
name, err := aqueousFocusedOutput(s.config.Seat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.captureOutput(name)
|
||||
}
|
||||
output := s.findFocusedOutput()
|
||||
if output == nil {
|
||||
s.outputsMu.Lock()
|
||||
|
||||
+1
@@ -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": []}
|
||||
@@ -56,6 +56,7 @@ type Output struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Seat string
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type AppChecker interface {
|
||||
@@ -115,3 +123,64 @@ func userBinDirs() []string {
|
||||
dirs = append(dirs, "/usr/local/bin")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -67,3 +70,58 @@ func TestEnvWithUserBinPathPrependsLocalBin(t *testing.T) {
|
||||
assert.Contains(t, parts, "/usr/local/bin")
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1863,6 +1863,8 @@ Item {
|
||||
function open(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
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];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
@@ -1885,6 +1887,8 @@ Item {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
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];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
|
||||
@@ -9,6 +9,8 @@ DankFloatingWindow {
|
||||
readonly property var log: Log.scoped("WorkspaceRenameModal")
|
||||
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
property var aqueousWorkspace: null
|
||||
property bool renaming: false
|
||||
|
||||
objectName: "workspaceRenameModal"
|
||||
title: I18n.tr("Rename Workspace")
|
||||
@@ -19,9 +21,24 @@ DankFloatingWindow {
|
||||
onClosed: hide()
|
||||
|
||||
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;
|
||||
visible = true;
|
||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||
return true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -29,6 +46,24 @@ DankFloatingWindow {
|
||||
}
|
||||
|
||||
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);
|
||||
hide();
|
||||
}
|
||||
@@ -49,6 +84,8 @@ DankFloatingWindow {
|
||||
return;
|
||||
}
|
||||
nameInput.text = "";
|
||||
aqueousWorkspace = null;
|
||||
renaming = false;
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
@@ -133,7 +170,7 @@ DankFloatingWindow {
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: I18n.tr("Workspace name")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
enabled: root.visible && !root.renaming
|
||||
onAccepted: submitAndClose()
|
||||
}
|
||||
}
|
||||
@@ -190,6 +227,7 @@ DankFloatingWindow {
|
||||
|
||||
MouseArea {
|
||||
id: renameArea
|
||||
enabled: !root.renaming
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
@@ -496,7 +496,7 @@ Item {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri && !CompositorService.isAqueous) {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
@@ -520,7 +520,7 @@ Item {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) {
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango && !CompositorService.isAqueous) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
@@ -795,7 +795,7 @@ Item {
|
||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, 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 showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow)
|
||||
|
||||
@@ -986,7 +986,7 @@ Item {
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||
const inOverviewWithShow = CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview;
|
||||
if (inOverviewWithShow)
|
||||
return true;
|
||||
|
||||
@@ -997,7 +997,7 @@ Item {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CompositorService.isNiri && NiriService.inOverview)
|
||||
if (CompositorService.overviewActiveOnScreen(barWindow.screenName))
|
||||
return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal;
|
||||
|
||||
return (barConfig?.visible ?? true) && (!autoHide || hoverReveal || popoutPinsReveal || revealSticky || ipcReveal);
|
||||
@@ -1062,7 +1062,7 @@ Item {
|
||||
top: barWindow.isVertical ? parent.top : 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
|
||||
acceptedButtons: barWindow.clickThroughEnabled ? Qt.NoButton : Qt.RightButton
|
||||
enabled: !inOverview && ((barConfig?.autoHide ?? false) || !barWindow.clickThroughEnabled)
|
||||
|
||||
@@ -156,6 +156,8 @@ Item {
|
||||
|
||||
function getRealWorkspaces() {
|
||||
const screenName = _barScreenName;
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return AqueousService.workspacesForOutput(SettingsData.workspaceFollowFocus ? AqueousService.focusedOutput : screenName);
|
||||
if (CompositorService.isNiri) {
|
||||
const fallbackWorkspaces = [
|
||||
{
|
||||
@@ -238,6 +240,8 @@ Item {
|
||||
|
||||
function getCurrentWorkspace() {
|
||||
const screenName = _barScreenName;
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return getRealWorkspaces().find(ws => ws.active)?.id || "";
|
||||
if (CompositorService.isNiri) {
|
||||
if (!screenName || SettingsData.workspaceFollowFocus) {
|
||||
return NiriService.getCurrentWorkspaceNumber();
|
||||
@@ -279,7 +283,14 @@ Item {
|
||||
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 currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs);
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||
@@ -323,7 +334,7 @@ Item {
|
||||
}
|
||||
|
||||
function switchApp(deltaY) {
|
||||
const windows = sortedToplevels;
|
||||
const windows = sortedToplevels.filter(w => !w.skipSwitcher);
|
||||
if (windows.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ PanelWindow {
|
||||
implicitWidth: body.surfaceImplicitWidth
|
||||
exclusiveZone: body.surfaceExclusiveZone
|
||||
|
||||
BackgroundEffect.blurRegion: body.blurRegion
|
||||
BackgroundEffect.blurRegion: BlurService.enabled ? body.blurRegion : null
|
||||
|
||||
Component.onCompleted: {
|
||||
KeyboardFocus.registerBarWindow(barWindow);
|
||||
|
||||
@@ -160,7 +160,7 @@ PanelWindow {
|
||||
return [];
|
||||
|
||||
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++) {
|
||||
const toplevel = allToplevels[i];
|
||||
if (toplevel.appId === root.appData.appId) {
|
||||
|
||||
@@ -31,7 +31,7 @@ BasePill {
|
||||
property int availableWidth: maxWidth
|
||||
readonly property real effectiveHorizontalWidth: Math.max(0, Math.min(maxWidth, availableWidth))
|
||||
readonly property real effectiveHorizontalInnerWidth: Math.max(0, effectiveHorizontalWidth - horizontalPadding * 2)
|
||||
property Toplevel activeWindow: null
|
||||
property var activeWindow: null
|
||||
property var activeDesktopEntry: null
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property bool isAutoHideBar: false
|
||||
@@ -99,6 +99,11 @@ BasePill {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (!active && CompositorService.isNiri) {
|
||||
|
||||
@@ -16,6 +16,8 @@ BasePill {
|
||||
property bool showIcon: widgetData?.keyboardLayoutNameShowIcon !== undefined ? widgetData.keyboardLayoutNameShowIcon : SettingsData.keyboardLayoutNameShowIcon
|
||||
readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"]
|
||||
property string currentLayout: {
|
||||
if (CompositorService.isAqueous)
|
||||
return AqueousService.keyboardLayout;
|
||||
if (CompositorService.isNiri) {
|
||||
return NiriService.getCurrentKeyboardLayoutName();
|
||||
} else if (CompositorService.isMango) {
|
||||
@@ -25,7 +27,7 @@ BasePill {
|
||||
}
|
||||
property string hyprlandKeyboard: ""
|
||||
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 verticalReserveLabel: widestLabel(_allLayoutLabels.map(n => LayoutCodes.layoutCode(n)))
|
||||
|
||||
@@ -131,6 +133,8 @@ BasePill {
|
||||
onClicked: {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.cycleKeyboardLayout();
|
||||
} else if (CompositorService.isAqueous) {
|
||||
AqueousService.cycleKeyboardLayout();
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
|
||||
} else if (CompositorService.isMango) {
|
||||
|
||||
@@ -163,7 +163,7 @@ BasePill {
|
||||
const deltaY = wheelEvent.angleDelta.y;
|
||||
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)
|
||||
return;
|
||||
|
||||
@@ -236,7 +236,7 @@ BasePill {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: _groupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: _groupByApp ? "appId" : "address"
|
||||
objectProp: _groupByApp ? "appId" : (CompositorService.isAqueous && AqueousService.available ? "aqueousKey" : "address")
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
@@ -494,7 +494,7 @@ BasePill {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: _groupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: _groupByApp ? "appId" : "address"
|
||||
objectProp: _groupByApp ? "appId" : (CompositorService.isAqueous && AqueousService.available ? "aqueousKey" : "address")
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
|
||||
@@ -24,6 +24,7 @@ Item {
|
||||
property real crossEdgeExtension: 0
|
||||
|
||||
readonly property bool isMango: CompositorService.isMango
|
||||
readonly property bool useAqueous: CompositorService.isAqueous && AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1"
|
||||
|
||||
readonly property real _leftMargin: {
|
||||
if (isVertical)
|
||||
@@ -82,8 +83,10 @@ Item {
|
||||
}
|
||||
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: {
|
||||
if (useAqueous)
|
||||
return false;
|
||||
if (Quickshell.env("DMS_FORCE_EXTWS") === "1")
|
||||
return (WindowManager.windowsets?.length ?? 0) > 0;
|
||||
if (!CompositorService.compositorDetected)
|
||||
@@ -117,6 +120,8 @@ Item {
|
||||
}
|
||||
|
||||
property var currentWorkspace: {
|
||||
if (useAqueous)
|
||||
return AqueousService.workspacesForOutput(effectiveScreenName).find(w => w.active)?.id || "";
|
||||
if (useExtWorkspace)
|
||||
return getExtWorkspaceActiveWorkspace();
|
||||
|
||||
@@ -143,6 +148,10 @@ Item {
|
||||
return [];
|
||||
}
|
||||
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) {
|
||||
const baseList = getExtWorkspaceWorkspaces();
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
|
||||
@@ -322,12 +331,14 @@ Item {
|
||||
|
||||
function getWorkspaceIcons(ws) {
|
||||
_desktopEntriesUpdateTrigger;
|
||||
if (!SettingsData.showWorkspaceApps || !ws) {
|
||||
if (!SettingsData.showWorkspaceApps || !ws || useExtWorkspace || (useAqueous && ws._placeholder)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let targetWorkspaceId;
|
||||
if (CompositorService.isNiri) {
|
||||
if (useAqueous) {
|
||||
targetWorkspaceId = ws.id;
|
||||
} else if (CompositorService.isNiri) {
|
||||
if (!ws || typeof ws !== "object") {
|
||||
const wsNumber = typeof ws === "number" ? ws : -1;
|
||||
if (wsNumber <= 0) {
|
||||
@@ -387,7 +398,9 @@ Item {
|
||||
return;
|
||||
} else {
|
||||
let winWs = null;
|
||||
if (CompositorService.isNiri) {
|
||||
if (useAqueous) {
|
||||
winWs = w.aqueousWorkspaceId;
|
||||
} else if (CompositorService.isNiri) {
|
||||
winWs = w.workspace_id;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
winWs = w.workspace?.num;
|
||||
@@ -405,7 +418,7 @@ Item {
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
|
||||
const moddedId = Paths.moddedAppId(keyBase);
|
||||
const groupThisWs = SettingsData.groupWorkspaceApps && (!isActiveWs || SettingsData.groupActiveWorkspaceApps);
|
||||
const key = groupThisWs ? moddedId : `${moddedId}_${i}`;
|
||||
const key = groupThisWs ? moddedId : (w.aqueousKey || `${moddedId}_${i}`);
|
||||
|
||||
if (!byApp[key]) {
|
||||
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)),
|
||||
"count": 1,
|
||||
"windowId": w.address || w.id,
|
||||
"windowSession": useAqueous ? w.aqueousSession : "",
|
||||
"fallbackText": appName || ""
|
||||
};
|
||||
} else {
|
||||
@@ -435,7 +449,7 @@ Item {
|
||||
}
|
||||
|
||||
function _makePlaceholder() {
|
||||
if (useExtWorkspace)
|
||||
if (useAqueous || useExtWorkspace)
|
||||
return {
|
||||
"id": "",
|
||||
"name": "",
|
||||
@@ -648,7 +662,7 @@ Item {
|
||||
if (!extProjection)
|
||||
return "";
|
||||
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
|
||||
@@ -659,7 +673,7 @@ Item {
|
||||
|
||||
function getRealWorkspaces() {
|
||||
return root.workspaceList.filter(ws => {
|
||||
if (useExtWorkspace)
|
||||
if (useAqueous || useExtWorkspace)
|
||||
return ws && !ws._placeholder;
|
||||
if (CompositorService.isNiri)
|
||||
return ws && ws.idx !== -1;
|
||||
@@ -674,9 +688,14 @@ Item {
|
||||
}
|
||||
|
||||
function switchToWorkspaceByModelData(data) {
|
||||
if (!data)
|
||||
if (!data || data._placeholder)
|
||||
return;
|
||||
|
||||
if (useAqueous) {
|
||||
AqueousService.activateWorkspace(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.useExtWorkspace) {
|
||||
if (typeof data.activate === "function")
|
||||
data.activate();
|
||||
@@ -727,13 +746,21 @@ Item {
|
||||
}
|
||||
|
||||
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) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
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 nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
||||
|
||||
@@ -812,6 +839,8 @@ Item {
|
||||
}
|
||||
|
||||
function getWorkspaceIndexFallback(modelData, index) {
|
||||
if (useAqueous)
|
||||
return modelData?.number ?? "";
|
||||
if (root.useExtWorkspace)
|
||||
return index + 1;
|
||||
if (CompositorService.isNiri)
|
||||
@@ -827,8 +856,8 @@ Item {
|
||||
|
||||
function getWorkspaceIndex(modelData, index) {
|
||||
let isPlaceholder;
|
||||
if (root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?.hidden === true;
|
||||
if (root.useAqueous || root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?._placeholder === true;
|
||||
} else if (CompositorService.isNiri) {
|
||||
isPlaceholder = modelData?.idx === -1;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
@@ -868,7 +897,7 @@ Item {
|
||||
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 shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
||||
|
||||
@@ -962,7 +991,9 @@ Item {
|
||||
const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
|
||||
switch (mouse.button) {
|
||||
case Qt.RightButton:
|
||||
if (CompositorService.isNiri) {
|
||||
if (CompositorService.isAqueous) {
|
||||
AqueousService.toggleOverview(root.effectiveScreenName);
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview();
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||
@@ -1144,8 +1175,10 @@ Item {
|
||||
}
|
||||
|
||||
property bool isActive: {
|
||||
if (root.useAqueous)
|
||||
return modelData?.active === true;
|
||||
if (root.useExtWorkspace)
|
||||
return (modelData?.id || modelData?.name) === root.currentWorkspace;
|
||||
return modelData === root.currentWorkspace;
|
||||
if (CompositorService.isNiri)
|
||||
return !!(modelData && modelData.idx === root.currentWorkspace);
|
||||
if (CompositorService.isHyprland)
|
||||
@@ -1157,6 +1190,8 @@ Item {
|
||||
return modelData === root.currentWorkspace;
|
||||
}
|
||||
property bool isOccupied: {
|
||||
if (root.useAqueous)
|
||||
return AqueousService.toplevels.some(w => w.aqueousWorkspaceId === modelData?.id);
|
||||
if (CompositorService.isHyprland)
|
||||
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
|
||||
if (root.isMango)
|
||||
@@ -1166,7 +1201,7 @@ Item {
|
||||
return false;
|
||||
}
|
||||
property bool isPlaceholder: {
|
||||
if (root.useExtWorkspace)
|
||||
if (root.useAqueous || root.useExtWorkspace)
|
||||
return !!(modelData && modelData._placeholder);
|
||||
if (CompositorService.isNiri)
|
||||
return !!(modelData && modelData.idx === -1);
|
||||
@@ -1183,6 +1218,8 @@ Item {
|
||||
property var loadedWorkspaceData: null
|
||||
property bool loadedIsUrgent: false
|
||||
property bool isUrgent: {
|
||||
if (root.useAqueous)
|
||||
return modelData?.urgent === true;
|
||||
if (root.useExtWorkspace)
|
||||
return modelData?.urgent ?? false;
|
||||
if (CompositorService.isHyprland)
|
||||
@@ -1210,6 +1247,9 @@ Item {
|
||||
if (!SettingsData.showWorkspaceApps || isPlaceholder)
|
||||
return 0;
|
||||
|
||||
if (root.useAqueous)
|
||||
return loadedIcons.length;
|
||||
|
||||
let targetWorkspaceId;
|
||||
if (root.useExtWorkspace) {
|
||||
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 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 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 contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0
|
||||
|
||||
@@ -1481,7 +1521,9 @@ Item {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (delegateRoot.focusWindowAt(mouse.x, mouse.y))
|
||||
return;
|
||||
if (root.useExtWorkspace) {
|
||||
if (root.useAqueous) {
|
||||
root.switchToWorkspaceByModelData(modelData);
|
||||
} else if (root.useExtWorkspace) {
|
||||
if (typeof modelData?.activate === "function")
|
||||
modelData.activate();
|
||||
} else if (CompositorService.isNiri) {
|
||||
@@ -1496,7 +1538,9 @@ Item {
|
||||
CompositorService.dispatchSwayWorkspace(modelData);
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
if (CompositorService.isAqueous) {
|
||||
AqueousService.toggleOverview(root.effectiveScreenName);
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview();
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||
@@ -1519,7 +1563,7 @@ Item {
|
||||
}
|
||||
|
||||
var wsData = null;
|
||||
if (root.useExtWorkspace) {
|
||||
if (root.useAqueous || root.useExtWorkspace) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isNiri) {
|
||||
wsData = modelData || null;
|
||||
@@ -1561,13 +1605,20 @@ Item {
|
||||
if (!layout)
|
||||
return null;
|
||||
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) {
|
||||
const winId = delegateRoot.windowIdAt(x, y);
|
||||
if (!winId)
|
||||
return false;
|
||||
if (root.useAqueous) {
|
||||
AqueousService.command("window.activate", winId);
|
||||
return true;
|
||||
}
|
||||
if (CompositorService.isHyprland) {
|
||||
HyprlandService.focusWindow(winId);
|
||||
return true;
|
||||
@@ -1770,6 +1821,7 @@ Item {
|
||||
width: root.appIconSize
|
||||
height: root.appIconSize
|
||||
readonly property var windowId: modelData.windowId
|
||||
readonly property string windowSession: modelData.windowSession || ""
|
||||
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||
@@ -1927,6 +1979,7 @@ Item {
|
||||
width: root.appIconSize
|
||||
height: root.appIconSize
|
||||
readonly property var windowId: modelData.windowId
|
||||
readonly property string windowSession: modelData.windowSession || ""
|
||||
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||
@@ -2123,6 +2176,13 @@ Item {
|
||||
delegateRoot.updateAllData();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: AqueousService
|
||||
function onStateChanged() {
|
||||
if (root.useAqueous)
|
||||
delegateRoot.updateAllData();
|
||||
}
|
||||
}
|
||||
property var _extWindowsetsTrigger: root.useExtWorkspace ? WindowManager.windowsets : null
|
||||
on_ExtWindowsetsTriggerChanged: {
|
||||
if (root.useExtWorkspace)
|
||||
|
||||
@@ -96,7 +96,7 @@ Item {
|
||||
return toplevel.activated;
|
||||
} else if (appData.type === "grouped") {
|
||||
// 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++) {
|
||||
const toplevel = allToplevels[i];
|
||||
if (toplevel.appId === appData.appId && toplevel.activated) {
|
||||
|
||||
@@ -262,7 +262,7 @@ Item {
|
||||
const separatePinnedAndRunning = SettingsData.dockSeparatePinnedAndRunningApps;
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
let uniqueKey = "window_" + index;
|
||||
let uniqueKey = toplevel.aqueousKey || "window_" + index;
|
||||
if (CompositorService.isHyprland && Hyprland.toplevels) {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values);
|
||||
for (let i = 0; i < hyprlandToplevels.length; i++) {
|
||||
|
||||
@@ -249,7 +249,7 @@ Item {
|
||||
readonly property bool shouldHideForWindows: {
|
||||
if (!SettingsData.dockSmartAutoHide)
|
||||
return false;
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango && !CompositorService.isAqueous)
|
||||
return false;
|
||||
|
||||
const screenName = dock.modelData?.name ?? "";
|
||||
@@ -313,6 +313,9 @@ Item {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return AqueousService.overlapsDock(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
|
||||
|
||||
if (CompositorService.isMango) {
|
||||
MangoService.windows;
|
||||
MangoService.outputs;
|
||||
@@ -348,7 +351,7 @@ Item {
|
||||
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
|
||||
|
||||
onOverviewRevealChanged: {
|
||||
|
||||
@@ -87,7 +87,7 @@ PanelWindow {
|
||||
|
||||
readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
|
||||
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)
|
||||
return false;
|
||||
const screens = SettingsData.getFilteredScreens("dock");
|
||||
|
||||
@@ -18,6 +18,7 @@ Item {
|
||||
property bool isMiracle: CompositorService.isMiracle
|
||||
property bool isMango: CompositorService.isMango
|
||||
property bool isLabwc: CompositorService.isLabwc
|
||||
property bool isAqueous: CompositorService.isAqueous
|
||||
|
||||
property string compositorName: {
|
||||
if (isHyprland)
|
||||
@@ -32,6 +33,8 @@ Item {
|
||||
return "mangowc";
|
||||
if (isLabwc)
|
||||
return "labwc";
|
||||
if (isAqueous)
|
||||
return "aqueous";
|
||||
return "niri";
|
||||
}
|
||||
|
||||
@@ -48,6 +51,8 @@ Item {
|
||||
return "/assets/mango.png";
|
||||
if (isLabwc)
|
||||
return "/assets/labwc.png";
|
||||
if (isAqueous)
|
||||
return "/assets/aqueous.svg";
|
||||
return "/assets/niri.svg";
|
||||
}
|
||||
|
||||
@@ -64,6 +69,8 @@ Item {
|
||||
return "https://github.com/DreamMaoMao/mangowc";
|
||||
if (isLabwc)
|
||||
return "https://labwc.github.io/";
|
||||
if (isAqueous)
|
||||
return "";
|
||||
return "https://github.com/niri-wm/niri";
|
||||
}
|
||||
|
||||
@@ -80,6 +87,8 @@ Item {
|
||||
return I18n.tr("mangowc GitHub");
|
||||
if (isLabwc)
|
||||
return I18n.tr("LabWC Website");
|
||||
if (isAqueous)
|
||||
return "Aqueous";
|
||||
return I18n.tr("niri GitHub");
|
||||
}
|
||||
|
||||
@@ -376,11 +385,15 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
cursorShape: compositorUrl === "" ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: parent.hovered = false
|
||||
onClicked: Qt.openUrlExternally(compositorUrl)
|
||||
onClicked: {
|
||||
if (compositorUrl === "")
|
||||
return;
|
||||
Qt.openUrlExternally(compositorUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -874,15 +874,15 @@ Item {
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop
|
||||
visible: (CompositorService.isNiri || CompositorService.isAqueous) && !dankBarTab.islandOwnsSelectedBarTop
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barOpenOnOverview"
|
||||
tags: ["bar", "overview", "niri", "show", "frame"]
|
||||
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop
|
||||
visible: (CompositorService.isNiri || CompositorService.isAqueous) && !dankBarTab.islandOwnsSelectedBarTop
|
||||
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)
|
||||
onToggled: toggled => {
|
||||
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 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 var currentOutputSet: []
|
||||
@@ -1707,9 +1898,21 @@ Singleton {
|
||||
MangoService.generateOutputsConfig(outputsData, finish);
|
||||
break;
|
||||
default:
|
||||
WlrOutputService.applyOutputsConfig(outputsData, outputs);
|
||||
finish(true);
|
||||
break;
|
||||
{
|
||||
if (_cancelOutputWrite)
|
||||
_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;
|
||||
}
|
||||
@@ -2145,11 +2348,15 @@ Singleton {
|
||||
originalDisplayNameMode = "";
|
||||
}
|
||||
|
||||
function restoreDisplayNameMode() {
|
||||
if (originalDisplayNameMode === "")
|
||||
return;
|
||||
SettingsData.displayNameMode = originalDisplayNameMode;
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
|
||||
function discardChanges() {
|
||||
if (originalDisplayNameMode !== "") {
|
||||
SettingsData.displayNameMode = originalDisplayNameMode;
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
restoreDisplayNameMode();
|
||||
backendFetchOutputs();
|
||||
clearPendingChanges();
|
||||
}
|
||||
@@ -2221,16 +2428,32 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
changesApplied(changeDescriptions);
|
||||
|
||||
if (formatChanged)
|
||||
SettingsData.saveSettings();
|
||||
|
||||
if (CompositorService.isHyprland)
|
||||
commitHyprlandSettingsChanges();
|
||||
if (CompositorService.isAqueous) {
|
||||
previewAqueousOutputs(changeDescriptions);
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -2341,6 +2564,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function confirmChanges(profileId) {
|
||||
if (CompositorService.isAqueous) {
|
||||
finishAqueousPreview(true);
|
||||
return;
|
||||
}
|
||||
const outputConfigs = buildCurrentOutputConfigs();
|
||||
lastAppliedEntry = {
|
||||
outputs: outputConfigs
|
||||
@@ -2372,14 +2599,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function revertChanges() {
|
||||
if (CompositorService.isAqueous && aqueousPreview) {
|
||||
finishAqueousPreview(false);
|
||||
return;
|
||||
}
|
||||
const hadFormatChange = originalDisplayNameMode !== "";
|
||||
const hadNiriChanges = originalNiriSettings !== null;
|
||||
const hadHyprlandChanges = originalHyprlandSettings !== null;
|
||||
|
||||
if (hadFormatChange) {
|
||||
SettingsData.displayNameMode = originalDisplayNameMode;
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
restoreDisplayNameMode();
|
||||
|
||||
if (hadNiriChanges) {
|
||||
SessionData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings));
|
||||
|
||||
@@ -99,6 +99,25 @@ Item {
|
||||
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 {
|
||||
width: parent.width
|
||||
height: profileSection.implicitHeight + Theme.spacingL * 2
|
||||
|
||||
@@ -70,7 +70,7 @@ Item {
|
||||
text: I18n.tr("Intelligent Auto-hide")
|
||||
description: I18n.tr("Show dock when floating windows don't overlap its area")
|
||||
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 => {
|
||||
if (checked && SettingsData.dockAutoHide) {
|
||||
SettingsData.set("dockAutoHide", false);
|
||||
@@ -83,9 +83,9 @@ Item {
|
||||
settingKey: "dockOpenOnOverview"
|
||||
tags: ["dock", "overview", "niri"]
|
||||
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
|
||||
visible: CompositorService.isNiri
|
||||
visible: CompositorService.isNiri || CompositorService.isAqueous
|
||||
onToggled: checked => SettingsData.set("dockOpenOnOverview", checked)
|
||||
}
|
||||
|
||||
|
||||
@@ -369,13 +369,13 @@ Item {
|
||||
settingKey: "frameBarIntegration"
|
||||
collapsible: true
|
||||
expanded: true
|
||||
visible: SettingsData.frameEnabled && CompositorService.isNiri
|
||||
visible: SettingsData.frameEnabled && (CompositorService.isNiri || CompositorService.isAqueous)
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "frameShowOnOverview"
|
||||
tags: ["frame", "overview", "show", "hide", "niri"]
|
||||
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
|
||||
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ Item {
|
||||
property bool recording: false
|
||||
property bool isNew: false
|
||||
property bool readOnly: false
|
||||
property bool saveBlocked: false
|
||||
property var retainedEdit: null
|
||||
readonly property bool retainEdits: retainedEdit !== null
|
||||
property string restoreKey: ""
|
||||
|
||||
property int editingKeyIndex: -1
|
||||
@@ -70,6 +73,7 @@ Item {
|
||||
signal removeBind(string key)
|
||||
signal resetBind(string key)
|
||||
signal cancelEdit
|
||||
signal editChanged
|
||||
|
||||
clip: true
|
||||
|
||||
@@ -91,7 +95,9 @@ Item {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (isNew && isExpanded)
|
||||
if (retainedEdit && isExpanded)
|
||||
restoreRetainedEdit();
|
||||
else if (isNew && isExpanded)
|
||||
resetEdits();
|
||||
}
|
||||
|
||||
@@ -99,6 +105,10 @@ Item {
|
||||
_userToggledExpand = true;
|
||||
if (!isExpanded)
|
||||
return;
|
||||
if (retainedEdit) {
|
||||
restoreRetainedEdit();
|
||||
return;
|
||||
}
|
||||
if (restoreKey) {
|
||||
restoreToKey(restoreKey);
|
||||
} else {
|
||||
@@ -112,6 +122,13 @@ Item {
|
||||
restoreToKey(restoreKey);
|
||||
}
|
||||
|
||||
function restoreRetainedEdit() {
|
||||
const retained = retainedEdit;
|
||||
editingKeyIndex = keys.findIndex(key => key.key === retained.originalKey);
|
||||
addingNewKey = !retained.originalKey;
|
||||
updateEdit(retained.data);
|
||||
}
|
||||
|
||||
function restoreToKey(keyToFind) {
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (keys[i].key === keyToFind) {
|
||||
@@ -151,7 +168,8 @@ Item {
|
||||
}
|
||||
hasChanges = false;
|
||||
_actionType = Actions.getActionType(editAction);
|
||||
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
||||
useCustomCompositor = _actionType === "compositor" && editAction && !KeybindsService.isKnownCompositorAction(editAction);
|
||||
editChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -175,7 +193,8 @@ Item {
|
||||
editAllowInhibiting = editingKeyIndex >= 0 ? keys[editingKeyIndex].allowInhibiting : undefined;
|
||||
hasChanges = false;
|
||||
_actionType = Actions.getActionType(editAction);
|
||||
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
||||
useCustomCompositor = _actionType === "compositor" && editAction && !KeybindsService.isKnownCompositorAction(editAction);
|
||||
editChanged();
|
||||
}
|
||||
|
||||
function startAddingNewKey() {
|
||||
@@ -187,6 +206,7 @@ Item {
|
||||
editingKeyIndex = -1;
|
||||
editKey = "";
|
||||
hasChanges = true;
|
||||
editChanged();
|
||||
}
|
||||
|
||||
function selectKeyForEdit(index) {
|
||||
@@ -202,6 +222,7 @@ Item {
|
||||
editRepeat = keys[index].repeat;
|
||||
editAllowInhibiting = keys[index].allowInhibiting;
|
||||
hasChanges = false;
|
||||
editChanged();
|
||||
}
|
||||
|
||||
function updateEdit(changes) {
|
||||
@@ -232,10 +253,11 @@ Item {
|
||||
const origRepeat = hasKey ? keys[editingKeyIndex].repeat : 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;
|
||||
editChanged();
|
||||
}
|
||||
|
||||
function canSave() {
|
||||
if (readOnly)
|
||||
if (readOnly || saveBlocked)
|
||||
return false;
|
||||
if (!editKey)
|
||||
return false;
|
||||
@@ -270,8 +292,10 @@ Item {
|
||||
"repeat": editRepeat,
|
||||
"allowInhibiting": editAllowInhibiting
|
||||
});
|
||||
hasChanges = false;
|
||||
addingNewKey = false;
|
||||
if (!retainEdits) {
|
||||
hasChanges = false;
|
||||
addingNewKey = false;
|
||||
}
|
||||
}
|
||||
|
||||
ShortcutInhibitor {
|
||||
@@ -1705,6 +1729,7 @@ Item {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingM
|
||||
visible: KeybindsService.currentProvider !== "aqueous"
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Title")
|
||||
@@ -2007,9 +2032,9 @@ Item {
|
||||
buttonHeight: root._buttonHeight
|
||||
backgroundColor: Theme.floatingWindowFieldColor
|
||||
textColor: Theme.surfaceText
|
||||
visible: root.hasChanges || root.isNew
|
||||
visible: root.hasChanges || root.isNew || root.retainEdits
|
||||
onClicked: {
|
||||
if (root.isNew) {
|
||||
if (root.isNew || root.retainEdits) {
|
||||
root.cancelEdit();
|
||||
} else {
|
||||
root.resetEdits();
|
||||
|
||||
@@ -27,8 +27,154 @@ Item {
|
||||
property bool _preserveScroll: false
|
||||
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() {
|
||||
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) {
|
||||
_filteredBinds = allBinds;
|
||||
return;
|
||||
@@ -80,6 +226,14 @@ Item {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -88,27 +242,59 @@ Item {
|
||||
KeybindsService.showHyprlandReadOnlyWarning();
|
||||
return;
|
||||
}
|
||||
if (KeybindsService.requiresBindReview) {
|
||||
if (!keybindsTab.beginEdit({
|
||||
action: "",
|
||||
desc: ""
|
||||
}, ""))
|
||||
return;
|
||||
newBindItem.resetEdits();
|
||||
}
|
||||
showingNewBind = true;
|
||||
expandedKey = "";
|
||||
}
|
||||
|
||||
function cancelNewBind() {
|
||||
if (keybindsTab.hasEditDraft) {
|
||||
keybindsTab.discardEdit();
|
||||
return;
|
||||
}
|
||||
showingNewBind = false;
|
||||
}
|
||||
|
||||
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;
|
||||
expandedKey = bindData.action;
|
||||
}
|
||||
|
||||
function confirmRemoveBind(key, remainingKey) {
|
||||
const draft = KeybindsService.requiresBindReview ? prepareRemoval(key, "remove") : null;
|
||||
if (KeybindsService.requiresBindReview && !draft)
|
||||
return;
|
||||
const baselineDraft = keybindsTab.editDraft;
|
||||
removeBindConfirm.showWithOptions({
|
||||
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),
|
||||
confirmText: I18n.tr("Remove"),
|
||||
confirmColor: Theme.primary,
|
||||
onConfirm: () => {
|
||||
if (draft) {
|
||||
if (keybindsTab.editDraft !== baselineDraft)
|
||||
return;
|
||||
keybindsTab.editDraft = draft;
|
||||
keybindsTab.submitEdit();
|
||||
return;
|
||||
}
|
||||
KeybindsService.removeBind(key);
|
||||
keybindsTab._editingKey = remainingKey;
|
||||
}
|
||||
@@ -116,18 +302,40 @@ Item {
|
||||
}
|
||||
|
||||
function confirmResetBind(key, remainingKey) {
|
||||
const draft = KeybindsService.requiresBindReview ? prepareRemoval(key, "reset") : null;
|
||||
if (KeybindsService.requiresBindReview && !draft)
|
||||
return;
|
||||
const baselineDraft = keybindsTab.editDraft;
|
||||
removeBindConfirm.showWithOptions({
|
||||
title: I18n.tr("Reset to default"),
|
||||
message: I18n.tr("Drop your override for %1 so the DMS default action re-applies?").arg(key),
|
||||
confirmText: I18n.tr("Reset"),
|
||||
confirmColor: Theme.primary,
|
||||
onConfirm: () => {
|
||||
if (draft) {
|
||||
if (keybindsTab.editDraft !== baselineDraft)
|
||||
return;
|
||||
keybindsTab.editDraft = draft;
|
||||
keybindsTab.submitEdit();
|
||||
return;
|
||||
}
|
||||
KeybindsService.resetBind(key);
|
||||
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() {
|
||||
if (showingNewBind) {
|
||||
showingNewBind = false;
|
||||
@@ -198,6 +406,11 @@ Item {
|
||||
function _ensureCurrentProvider() {
|
||||
if (!KeybindsService.available)
|
||||
return;
|
||||
if (KeybindsService.requiresBindReview) {
|
||||
if (!keybindsTab.hasEditDraft && !KeybindsService.bindMutationBusy)
|
||||
KeybindsService.loadBinds(false);
|
||||
return;
|
||||
}
|
||||
const cachedProvider = KeybindsService.keybinds?.provider;
|
||||
const targetProvider = KeybindsService.currentProvider;
|
||||
if (cachedProvider !== targetProvider || KeybindsService._dataVersion === 0) {
|
||||
@@ -296,8 +509,8 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
readonly property string bindsFile: 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)
|
||||
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.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
|
||||
color: Theme.surfaceVariantText
|
||||
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 {
|
||||
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||
height: newBindSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -545,6 +808,7 @@ Item {
|
||||
}
|
||||
|
||||
KeybindItem {
|
||||
id: newBindItem
|
||||
width: parent.width
|
||||
isNew: true
|
||||
isExpanded: true
|
||||
@@ -561,6 +825,17 @@ Item {
|
||||
})
|
||||
panelWindow: keybindsTab.parentModal
|
||||
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)
|
||||
onCancelEdit: keybindsTab.cancelNewBind()
|
||||
}
|
||||
@@ -670,13 +945,25 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
bindData: modelData
|
||||
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
|
||||
readOnly: KeybindsService.readOnly
|
||||
onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
|
||||
onSaveBind: (originalKey, newData) => {
|
||||
KeybindsService.saveBind(originalKey, newData);
|
||||
keybindsTab._editingKey = newData.key;
|
||||
keybindsTab.expandedKey = newData.action;
|
||||
keybindsTab.saveBind(originalKey, newData);
|
||||
}
|
||||
onRemoveBind: key => {
|
||||
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
||||
|
||||
@@ -455,6 +455,17 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
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 {
|
||||
tab: "theme"
|
||||
tags: ["color", "palette", "theme", "appearance"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
@@ -55,6 +56,16 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
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 {
|
||||
tab: "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();
|
||||
}
|
||||
|
||||
BackgroundEffect.blurRegion: visible ? menuBlurRegion : null
|
||||
BackgroundEffect.blurRegion: visible && BlurService.enabled ? menuBlurRegion : null
|
||||
|
||||
Region {
|
||||
id: menuBlurRegion
|
||||
|
||||
@@ -183,7 +183,7 @@ SettingsCard {
|
||||
"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
|
||||
|
||||
function isFocusedAppearanceSection(section) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -61,7 +62,8 @@ Item {
|
||||
text: I18n.tr("Show Workspace Apps")
|
||||
description: I18n.tr("Display application icons in workspace indicators")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -153,7 +155,7 @@ Item {
|
||||
text: I18n.tr("Follow Monitor Focus")
|
||||
description: I18n.tr("Show workspaces of the currently focused monitor")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -163,7 +165,8 @@ Item {
|
||||
text: I18n.tr("Show Occupied Workspaces Only")
|
||||
description: I18n.tr("Display only workspaces that contain windows")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -173,7 +176,7 @@ Item {
|
||||
text: I18n.tr("Reverse Scrolling Direction")
|
||||
description: I18n.tr("Reverse workspace switch direction when scrolling over the bar")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,9 +135,11 @@ Singleton {
|
||||
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() {
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return AqueousService.focusedOutput;
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace?.monitor)
|
||||
return Hyprland.focusedWorkspace.monitor.name;
|
||||
if (CompositorService.isNiri && NiriService.currentOutput)
|
||||
|
||||
@@ -21,8 +21,47 @@ Singleton {
|
||||
property bool isScroll: false
|
||||
property bool isMiracle: false
|
||||
property bool isLabwc: false
|
||||
property bool isAqueous: false
|
||||
property string compositor: "unknown"
|
||||
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 useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1"
|
||||
|
||||
@@ -45,9 +84,19 @@ Singleton {
|
||||
|
||||
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) {
|
||||
if (isAqueous && toplevel?.aqueousWindowId)
|
||||
return AqueousService.available && !AqueousService.locked && AqueousService.capabilities.commands && toplevel.canMinimize;
|
||||
return supportsMinimize && toplevel && toplevel.minimized !== undefined;
|
||||
}
|
||||
|
||||
@@ -59,6 +108,10 @@ Singleton {
|
||||
function activateToplevel(toplevel) {
|
||||
if (!toplevel)
|
||||
return;
|
||||
if (isAqueous && toplevel.aqueousWindowId) {
|
||||
toplevel.activate();
|
||||
return;
|
||||
}
|
||||
closeNiriOverviewOnWindowFocus();
|
||||
if (canMinimize(toplevel) && toplevel.minimized)
|
||||
toplevel.minimized = false;
|
||||
@@ -147,7 +200,9 @@ Singleton {
|
||||
|
||||
function getFocusedScreen() {
|
||||
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;
|
||||
else if (isNiri && NiriService.currentOutput)
|
||||
screenName = NiriService.currentOutput;
|
||||
@@ -267,6 +322,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function computeSortedToplevels() {
|
||||
if (isAqueous && AqueousService.available)
|
||||
return AqueousService.toplevels;
|
||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
|
||||
return [];
|
||||
|
||||
@@ -517,6 +574,10 @@ Singleton {
|
||||
}
|
||||
|
||||
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)
|
||||
return NiriService.filterCurrentWorkspace(toplevels, screen);
|
||||
if (useMangoSorting)
|
||||
@@ -528,6 +589,8 @@ Singleton {
|
||||
|
||||
function fullscreenToplevelOnScreen(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)
|
||||
return false;
|
||||
|
||||
@@ -657,6 +720,12 @@ Singleton {
|
||||
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) {
|
||||
return FrameTransitionState.effectiveConnectedFrameModeActive && frameWindowVisibleForScreen(screenOrName);
|
||||
}
|
||||
@@ -930,6 +999,8 @@ Singleton {
|
||||
return "miracle";
|
||||
case "labwc":
|
||||
return "labwc";
|
||||
case "aqueous":
|
||||
return "aqueous";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -943,6 +1014,7 @@ Singleton {
|
||||
isScroll = name === "scroll";
|
||||
isMiracle = name === "miracle";
|
||||
isLabwc = name === "labwc";
|
||||
isAqueous = name === "aqueous";
|
||||
compositor = name;
|
||||
compositorDetected = true;
|
||||
if (isNiri)
|
||||
@@ -994,7 +1066,14 @@ Singleton {
|
||||
// of winning on a stale env var.
|
||||
function _envDetectionCandidates() {
|
||||
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || "";
|
||||
const aqueousSocket = Quickshell.env("AQUEOUS_SOCKET") || "";
|
||||
return [
|
||||
{
|
||||
name: "aqueous",
|
||||
present: !!aqueousSocket,
|
||||
test: ["test", "-S", aqueousSocket],
|
||||
detail: "AQUEOUS_SOCKET " + aqueousSocket
|
||||
},
|
||||
{
|
||||
name: "mango",
|
||||
present: !!mangoSignature,
|
||||
@@ -1076,6 +1155,11 @@ Singleton {
|
||||
}
|
||||
if (isLabwc) {
|
||||
Quickshell.execDetached(["dms", "dpms", "off"]);
|
||||
return;
|
||||
}
|
||||
if (outputPowerAvailable) {
|
||||
setOutputPower(false);
|
||||
return;
|
||||
}
|
||||
log.warn("Cannot power off monitors, unknown compositor");
|
||||
}
|
||||
@@ -1095,6 +1179,11 @@ Singleton {
|
||||
}
|
||||
if (isLabwc) {
|
||||
Quickshell.execDetached(["dms", "dpms", "on"]);
|
||||
return;
|
||||
}
|
||||
if (outputPowerAvailable) {
|
||||
setOutputPower(true);
|
||||
return;
|
||||
}
|
||||
log.warn("Cannot power on monitors, unknown compositor");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
log.warn("DMSService.sendRequest: Not connected, method:", method);
|
||||
if (callback) {
|
||||
@@ -424,8 +433,19 @@ Singleton {
|
||||
request.params = params;
|
||||
}
|
||||
|
||||
if (callback)
|
||||
pendingRequests[id] = callback;
|
||||
if (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")) {
|
||||
clipboardRequestIds[id] = true;
|
||||
|
||||
@@ -14,8 +14,10 @@ Singleton {
|
||||
id: root
|
||||
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: {
|
||||
if (CompositorService.isAqueous)
|
||||
return "aqueous";
|
||||
if (CompositorService.isNiri)
|
||||
return "niri";
|
||||
if (CompositorService.isHyprland)
|
||||
@@ -26,6 +28,8 @@ Singleton {
|
||||
}
|
||||
|
||||
readonly property string cheatsheetProvider: {
|
||||
if (CompositorService.isAqueous)
|
||||
return "aqueous";
|
||||
if (CompositorService.isNiri)
|
||||
return "niri";
|
||||
if (CompositorService.isHyprland)
|
||||
@@ -66,6 +70,23 @@ Singleton {
|
||||
property var displayList: []
|
||||
property int _dataVersion: 0
|
||||
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 string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
@@ -119,7 +140,7 @@ Singleton {
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onCompositorChanged() {
|
||||
if (!CompositorService.isNiri && !CompositorService.isMango)
|
||||
if (!CompositorService.isNiri && !CompositorService.isMango && !CompositorService.isAqueous)
|
||||
return;
|
||||
Qt.callLater(root.loadBinds);
|
||||
}
|
||||
@@ -161,9 +182,12 @@ Singleton {
|
||||
Process {
|
||||
id: loadProcess
|
||||
running: false
|
||||
property string provider: ""
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (loadProcess.provider !== root.currentProvider)
|
||||
return;
|
||||
try {
|
||||
root._rawData = JSON.parse(text);
|
||||
root._processData();
|
||||
@@ -175,6 +199,8 @@ Singleton {
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (provider !== root.currentProvider)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Load process failed with code:", exitCode);
|
||||
root.loading = false;
|
||||
@@ -185,9 +211,13 @@ Singleton {
|
||||
Process {
|
||||
id: saveProcess
|
||||
running: false
|
||||
property string savedKey: ""
|
||||
property string provider: ""
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (saveProcess.provider !== root.currentProvider)
|
||||
return;
|
||||
if (!text.trim())
|
||||
return;
|
||||
root.lastError = text.trim();
|
||||
@@ -197,12 +227,20 @@ Singleton {
|
||||
|
||||
onExited: exitCode => {
|
||||
root.saving = false;
|
||||
if (provider !== root.currentProvider) {
|
||||
savedKey = "";
|
||||
return;
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
root._pendingSavedKey = "";
|
||||
savedKey = "";
|
||||
log.error("Save failed with code:", exitCode);
|
||||
root.bindSaveCompleted(false);
|
||||
return;
|
||||
}
|
||||
root.lastError = "";
|
||||
root._pendingSavedKey = savedKey;
|
||||
savedKey = "";
|
||||
root.bindSaveCompleted(true);
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
@@ -213,9 +251,12 @@ Singleton {
|
||||
Process {
|
||||
id: removeProcess
|
||||
running: false
|
||||
property string provider: ""
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (removeProcess.provider !== root.currentProvider)
|
||||
return;
|
||||
if (!text.trim())
|
||||
return;
|
||||
root.lastError = text.trim();
|
||||
@@ -224,6 +265,8 @@ Singleton {
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (provider !== root.currentProvider)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
log.error("Remove failed with code:", exitCode);
|
||||
return;
|
||||
@@ -339,14 +382,313 @@ Singleton {
|
||||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
const hasData = Object.keys(_allBinds).length > 0;
|
||||
loading = showLoading !== false && !hasData;
|
||||
loadProcess.command = ["dms", "keybinds", "show", currentProvider];
|
||||
loadProcess.provider = currentProvider;
|
||||
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() {
|
||||
keybinds = _rawData || {};
|
||||
modKey = currentProvider === "niri" ? (_rawData?.modKey || "Super") : "Super";
|
||||
@@ -504,7 +846,11 @@ Singleton {
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveBind(originalKey, bindData) {
|
||||
function saveBind(originalKey, bindData, draft, callback) {
|
||||
if (currentProvider === "aqueous") {
|
||||
_mutateAqueous(draft, callback || (() => {}));
|
||||
return;
|
||||
}
|
||||
if (readOnly) {
|
||||
showHyprlandReadOnlyWarning();
|
||||
return;
|
||||
@@ -512,7 +858,8 @@ Singleton {
|
||||
if (!bindData.key || !Actions.isValidAction(bindData.action))
|
||||
return;
|
||||
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)
|
||||
cmd.push("--replace-key", originalKey);
|
||||
if (bindData.cooldownMs > 0)
|
||||
@@ -526,8 +873,9 @@ Singleton {
|
||||
if (bindData.flags)
|
||||
cmd.push("--flags", bindData.flags);
|
||||
saveProcess.command = cmd;
|
||||
saveProcess.provider = currentProvider;
|
||||
saveProcess.savedKey = bindData.key;
|
||||
saveProcess.running = true;
|
||||
_pendingSavedKey = bindData.key;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
function removeBind(key) {
|
||||
function removeBind(key, draft, callback) {
|
||||
if (currentProvider === "aqueous") {
|
||||
_mutateAqueous(draft, callback || (() => {}));
|
||||
return;
|
||||
}
|
||||
if (readOnly) {
|
||||
showHyprlandReadOnlyWarning();
|
||||
return;
|
||||
@@ -560,11 +912,16 @@ Singleton {
|
||||
if (!key)
|
||||
return;
|
||||
removeProcess.command = ["dms", "keybinds", "remove", currentProvider, key];
|
||||
removeProcess.provider = currentProvider;
|
||||
removeProcess.running = true;
|
||||
bindRemoved(key);
|
||||
}
|
||||
|
||||
function resetBind(key) {
|
||||
function resetBind(key, draft, callback) {
|
||||
if (currentProvider === "aqueous") {
|
||||
_mutateAqueous(draft, callback || (() => {}));
|
||||
return;
|
||||
}
|
||||
if (readOnly) {
|
||||
showHyprlandReadOnlyWarning();
|
||||
return;
|
||||
@@ -572,19 +929,42 @@ Singleton {
|
||||
if (!key)
|
||||
return;
|
||||
removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key];
|
||||
removeProcess.provider = currentProvider;
|
||||
removeProcess.running = true;
|
||||
bindRemoved(key);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function isKnownCompositorAction(action) {
|
||||
if (currentProvider === "aqueous")
|
||||
return (_rawData?.binds?.Compositor || []).some(b => b.action === action);
|
||||
return Actions.isKnownCompositorAction(currentProvider, action);
|
||||
}
|
||||
|
||||
function getCompositorCategories() {
|
||||
if (currentProvider === "aqueous")
|
||||
return ["Compositor"];
|
||||
return Actions.getCompositorCategories(currentProvider);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -176,20 +176,20 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: uwsmLogout
|
||||
property bool notRunning: false
|
||||
command: ["uwsm", "stop"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
if (data.trim().toLowerCase().includes("not running")) {
|
||||
_logout();
|
||||
}
|
||||
if (data.trim().toLowerCase().includes("not running"))
|
||||
uwsmLogout.notRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
if (exitCode === 0 && !notRunning) {
|
||||
return;
|
||||
}
|
||||
_logout();
|
||||
@@ -393,13 +393,21 @@ Singleton {
|
||||
// * Session management
|
||||
function logout() {
|
||||
if (hasUwsm) {
|
||||
if (uwsmLogout.running)
|
||||
return;
|
||||
uwsmLogout.notRunning = false;
|
||||
uwsmLogout.running = true;
|
||||
return;
|
||||
}
|
||||
_logout();
|
||||
}
|
||||
|
||||
function _logout() {
|
||||
if (SettingsData.customPowerActionLogout.length === 0) {
|
||||
if (CompositorService.isAqueous) {
|
||||
AqueousService.quit();
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.quit();
|
||||
return;
|
||||
|
||||
@@ -44,6 +44,10 @@ Singleton {
|
||||
"isNiri": () => CompositorService.isNiri,
|
||||
"isHyprland": () => CompositorService.isHyprland,
|
||||
"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,
|
||||
"windowRulesCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
|
||||
"layoutCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
@@ -109,7 +110,7 @@ Singleton {
|
||||
function applyConfiguration(heads, callback) {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
if (callback) {
|
||||
callback(false, "Not connected");
|
||||
callback(false, I18n.tr("Not connected"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -122,11 +123,11 @@ Singleton {
|
||||
DMSService.sendRequest("wlroutput.applyConfiguration", {
|
||||
"heads": heads
|
||||
}, response => {
|
||||
const success = !response.error;
|
||||
const success = !response.error && response.result?.success === true;
|
||||
const message = response.error || response.result?.message || "";
|
||||
|
||||
if (response.error) {
|
||||
log.warn("applyConfiguration error:", response.error);
|
||||
if (!success) {
|
||||
log.warn("applyConfiguration error:", message);
|
||||
} else {
|
||||
log.debug("Configuration applied successfully");
|
||||
}
|
||||
@@ -135,13 +136,13 @@ Singleton {
|
||||
if (callback) {
|
||||
callback(success, message);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function testConfiguration(heads, callback) {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
if (callback) {
|
||||
callback(false, "Not connected");
|
||||
callback(false, I18n.tr("Not connected"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -151,11 +152,11 @@ Singleton {
|
||||
DMSService.sendRequest("wlroutput.testConfiguration", {
|
||||
"heads": heads
|
||||
}, response => {
|
||||
const success = !response.error;
|
||||
const success = !response.error && response.result?.success === true;
|
||||
const message = response.error || response.result?.message || "";
|
||||
|
||||
if (response.error) {
|
||||
log.warn("testConfiguration error:", response.error);
|
||||
if (!success) {
|
||||
log.warn("testConfiguration error:", message);
|
||||
} else {
|
||||
log.debug("Configuration test passed");
|
||||
}
|
||||
@@ -163,7 +164,7 @@ Singleton {
|
||||
if (callback) {
|
||||
callback(success, message);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function setOutputEnabled(outputName, enabled, callback) {
|
||||
@@ -282,9 +283,22 @@ Singleton {
|
||||
// NiriService, HyprlandService and MangoService. Instead of writing a
|
||||
// config file, the changes are applied directly via the
|
||||
// wlr-output-management protocol.
|
||||
function applyOutputsConfig(outputsData, connectedOutputs) {
|
||||
if (!wlrOutputAvailable)
|
||||
function applyOutputsConfig(outputsData, connectedOutputs, callback) {
|
||||
if (!wlrOutputAvailable) {
|
||||
if (callback)
|
||||
callback(false, I18n.tr("Not connected"));
|
||||
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 = [];
|
||||
for (const name in outputsData) {
|
||||
if (!connectedOutputs[name])
|
||||
@@ -319,8 +333,7 @@ Singleton {
|
||||
heads.push(head);
|
||||
}
|
||||
|
||||
if (heads.length > 0)
|
||||
applyConfiguration(heads);
|
||||
return heads;
|
||||
}
|
||||
|
||||
function transformFromName(name) {
|
||||
|
||||
@@ -20,7 +20,7 @@ Item {
|
||||
property string customPath: ""
|
||||
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 string resolvedMode: {
|
||||
const fallback = fallbackToApps ? "apps" : "";
|
||||
@@ -53,6 +53,8 @@ Item {
|
||||
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
|
||||
case "labwc":
|
||||
return "file://" + Theme.shellDir + "/assets/labwc.png";
|
||||
case "aqueous":
|
||||
return "file://" + Theme.shellDir + "/assets/aqueous.svg";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ Item {
|
||||
onClipHeightChanged: settleKickAction.restart()
|
||||
|
||||
function _runSettleKick() {
|
||||
if (!targetWindow?.visible)
|
||||
if (!BlurService.compositorSupported || !targetWindow?.visible)
|
||||
return;
|
||||
kick();
|
||||
settleRepeatTimer.restart();
|
||||
|
||||
@@ -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")
|
||||
condition_key = FILE_CONDITION_MAP.get(filename)
|
||||
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"
|
||||
elif "CompositorService.isNiri" in visible_raw:
|
||||
condition_key = "isNiri"
|
||||
|
||||
@@ -1240,7 +1240,6 @@
|
||||
"category": "Bar",
|
||||
"keywords": [
|
||||
"bar",
|
||||
"during",
|
||||
"frame",
|
||||
"niri",
|
||||
"overview",
|
||||
@@ -1249,8 +1248,8 @@
|
||||
"statusbar",
|
||||
"topbar"
|
||||
],
|
||||
"description": "Show during Niri overview",
|
||||
"conditionKey": "isNiri"
|
||||
"description": "Show on Overview",
|
||||
"conditionKey": "nativeOverviewCapable"
|
||||
},
|
||||
{
|
||||
"section": "barSize",
|
||||
@@ -1463,7 +1462,7 @@
|
||||
"workspaces"
|
||||
],
|
||||
"description": "Show workspaces of the currently focused monitor",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "workspaceFollowFocusCapable"
|
||||
},
|
||||
{
|
||||
"section": "workspaceSettings",
|
||||
@@ -1488,7 +1487,7 @@
|
||||
],
|
||||
"icon": "view_module",
|
||||
"description": "Show workspace index numbers in the top bar workspace switcher",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "groupActiveWorkspaceApps",
|
||||
@@ -1611,7 +1610,7 @@
|
||||
"workspace"
|
||||
],
|
||||
"description": "Reverse workspace switch direction when scrolling over the bar",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "workspaceUnfocusedMonitorSeparateAppearance",
|
||||
@@ -1696,7 +1695,7 @@
|
||||
"workspaces"
|
||||
],
|
||||
"description": "Display only workspaces that contain windows",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "showWorkspaceApps",
|
||||
@@ -1725,7 +1724,7 @@
|
||||
"workspace"
|
||||
],
|
||||
"description": "Display application icons in workspace indicators",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "showWorkspaceIndex",
|
||||
@@ -2015,7 +2014,7 @@
|
||||
"windows"
|
||||
],
|
||||
"description": "Show dock when floating windows don",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "dockIsolateDisplays",
|
||||
@@ -2311,17 +2310,15 @@
|
||||
"tabIndex": 5,
|
||||
"category": "Dock",
|
||||
"keywords": [
|
||||
"always",
|
||||
"dock",
|
||||
"launcher bar",
|
||||
"niri",
|
||||
"overview",
|
||||
"panel",
|
||||
"show",
|
||||
"taskbar"
|
||||
],
|
||||
"description": "Always show the dock when niri",
|
||||
"conditionKey": "isNiri"
|
||||
"description": "Show on Overview",
|
||||
"conditionKey": "nativeOverviewCapable"
|
||||
},
|
||||
{
|
||||
"section": "dockLauncherLogoSizeOffset",
|
||||
@@ -3615,6 +3612,23 @@
|
||||
"icon": "apps",
|
||||
"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",
|
||||
"label": "Auto From Wallpaper",
|
||||
@@ -6254,6 +6268,24 @@
|
||||
"icon": "animation",
|
||||
"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",
|
||||
"label": "Custom Duration",
|
||||
@@ -9962,7 +9994,6 @@
|
||||
"keywords": [
|
||||
"border",
|
||||
"decoration",
|
||||
"during",
|
||||
"frame",
|
||||
"hide",
|
||||
"integrations",
|
||||
@@ -9972,8 +10003,8 @@
|
||||
"window"
|
||||
],
|
||||
"icon": "toolbar",
|
||||
"description": "Show during Niri overview",
|
||||
"conditionKey": "isNiri"
|
||||
"description": "Show on Overview",
|
||||
"conditionKey": "nativeOverviewCapable"
|
||||
},
|
||||
{
|
||||
"section": "frameLauncherEmergeSide",
|
||||
@@ -10056,7 +10087,6 @@
|
||||
"keywords": [
|
||||
"border",
|
||||
"decoration",
|
||||
"during",
|
||||
"frame",
|
||||
"hide",
|
||||
"niri",
|
||||
@@ -10064,7 +10094,7 @@
|
||||
"show",
|
||||
"window"
|
||||
],
|
||||
"description": "Show during Niri overview",
|
||||
"description": "Show on Overview",
|
||||
"conditionKey": "frameEnabled"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user