From c3fd526698e0bf04db7160389aaf83a911745abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Thi=E1=BB=87n=20L=E1=BB=99c?= Date: Tue, 18 Aug 2026 21:38:06 +0700 Subject: [PATCH] perf(ipc): call Quickshell directly (#3081) * perf(ipc): call Quickshell directly * fix(ipc): handle indexed responses * fix(ipc): follow error string lint * fix(ipc): fall back to qs when the direct socket call fails --------- Co-authored-by: bbedward --- core/cmd/dms/shell.go | 25 +++++ core/internal/qsipc/client.go | 156 +++++++++++++++++++++++++++++ core/internal/qsipc/client_test.go | 58 +++++++++++ 3 files changed, 239 insertions(+) create mode 100644 core/internal/qsipc/client.go create mode 100644 core/internal/qsipc/client_test.go diff --git a/core/cmd/dms/shell.go b/core/cmd/dms/shell.go index cdbe39e2d..8743ef022 100644 --- a/core/cmd/dms/shell.go +++ b/core/cmd/dms/shell.go @@ -14,6 +14,7 @@ import ( "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/qsipc" ) type ipcTargets map[string]map[string][]string @@ -137,6 +138,9 @@ func runShellIPCCommand(args []string) { if args[0] != "call" { args = append([]string{"call"}, args...) } + if tryDirectIPCCall(args) { + return + } baseArgs, err := buildQsIPCBaseArgs() if err != nil { @@ -153,6 +157,27 @@ func runShellIPCCommand(args []string) { } } +// tryDirectIPCCall invokes the shell over its IPC socket without spawning qs. +// Any failure falls back to the qs child process path, which owns error output. +func tryDirectIPCCall(args []string) bool { + if len(args) < 3 { + return false + } + pid, ok := shellApp.SessionPID() + if !ok { + return false + } + result, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), args[1], args[2], args[3:]) + if err != nil { + log.Debugf("Direct Quickshell IPC failed, falling back to qs: %v", err) + return false + } + if !isVoid { + fmt.Println(result) + } + return true +} + func printIPCHelp() { fmt.Println("Usage: dms ipc call [args...]") fmt.Println() diff --git a/core/internal/qsipc/client.go b/core/internal/qsipc/client.go new file mode 100644 index 000000000..cec86117c --- /dev/null +++ b/core/internal/qsipc/client.go @@ -0,0 +1,156 @@ +package qsipc + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" + "path/filepath" + "time" + "unicode/utf16" +) + +const ( + commandStringCall = 3 + + // Keep these indexes in sync with quickshell-mirror/quickshell: + // https://github.com/quickshell-mirror/quickshell/blob/59e9c47/src/io/ipccomm.cpp#L126 + responseNotReady = 1 + responseTargetNotFound = 2 + responseFunctionNotFound = 3 + responseArgumentMismatch = 4 + responseCompleted = 5 + maxStringLength = 1 << 20 +) + +var ( + ErrNotReady = errors.New("quickshell IPC not ready") + ErrTargetNotFound = errors.New("quickshell IPC target not found") + ErrFunctionNotFound = errors.New("quickshell IPC function not found") + ErrArgumentMismatch = errors.New("quickshell IPC argument mismatch") + ErrUnsupportedResponse = errors.New("unsupported Quickshell IPC response") +) + +// Call invokes a Quickshell IpcHandler without starting a qs child process. +func Call(socketPath, target, function string, args []string) (string, bool, error) { + conn, err := net.DialTimeout("unix", socketPath, 2*time.Second) + if err != nil { + return "", false, fmt.Errorf("connect to Quickshell IPC: %w", err) + } + defer conn.Close() + + _ = conn.SetDeadline(time.Now().Add(2 * time.Second)) + if err := writeCall(conn, target, function, args); err != nil { + return "", false, fmt.Errorf("write Quickshell IPC request: %w", err) + } + + result, isVoid, err := readResponse(bufio.NewReader(conn)) + if err != nil { + return "", false, err + } + return result, isVoid, nil +} + +func writeCall(w io.Writer, target, function string, args []string) error { + var buf bytes.Buffer + if err := buf.WriteByte(commandStringCall); err != nil { + return err + } + if err := writeQString(&buf, target); err != nil { + return err + } + if err := writeQString(&buf, function); err != nil { + return err + } + if err := binary.Write(&buf, binary.BigEndian, uint32(len(args))); err != nil { + return err + } + for _, arg := range args { + if err := writeQString(&buf, arg); err != nil { + return err + } + } + _, err := w.Write(buf.Bytes()) + return err +} + +func readResponse(r io.Reader) (string, bool, error) { + var index uint8 + if err := binary.Read(r, binary.BigEndian, &index); err != nil { + return "", false, fmt.Errorf("read Quickshell IPC response: %w", err) + } + switch index { + case responseNotReady: + return "", false, ErrNotReady + case responseTargetNotFound: + return "", false, ErrTargetNotFound + case responseFunctionNotFound: + return "", false, ErrFunctionNotFound + case responseArgumentMismatch: + return "", false, ErrArgumentMismatch + case responseCompleted: + break + default: + return "", false, fmt.Errorf("%w: variant index %d", ErrUnsupportedResponse, index) + } + + var isVoid uint8 + if err := binary.Read(r, binary.BigEndian, &isVoid); err != nil { + return "", false, fmt.Errorf("read Quickshell IPC completion: %w", err) + } + value, err := readQString(r) + if err != nil { + return "", false, fmt.Errorf("read Quickshell IPC result: %w", err) + } + return value, isVoid != 0, nil +} + +func writeQString(w io.Writer, value string) error { + encoded := utf16.Encode([]rune(value)) + byteLength := len(encoded) * 2 + if byteLength > maxStringLength { + return fmt.Errorf("QString is too large: %d bytes", byteLength) + } + if err := binary.Write(w, binary.BigEndian, uint32(byteLength)); err != nil { + return err + } + for _, codeUnit := range encoded { + if err := binary.Write(w, binary.BigEndian, codeUnit); err != nil { + return err + } + } + return nil +} + +func readQString(r io.Reader) (string, error) { + var length uint32 + if err := binary.Read(r, binary.BigEndian, &length); err != nil { + return "", err + } + if length == ^uint32(0) { + return "", nil + } + if length > maxStringLength || length%2 != 0 { + return "", fmt.Errorf("invalid QString length: %d bytes", length) + } + codeUnits := make([]uint16, length/2) + for i := range codeUnits { + if err := binary.Read(r, binary.BigEndian, &codeUnits[i]); err != nil { + return "", err + } + } + return string(utf16.Decode(codeUnits)), nil +} + +// SocketPathForPID returns the instance IPC socket used by qs --pid. +func SocketPathForPID(pid int) string { + runtimeDir := os.Getenv("XDG_RUNTIME_DIR") + if runtimeDir == "" { + runtimeDir = fmt.Sprintf("/run/user/%d", os.Getuid()) + } + return filepath.Join(runtimeDir, "quickshell", "by-pid", fmt.Sprint(pid), "ipc.sock") +} diff --git a/core/internal/qsipc/client_test.go b/core/internal/qsipc/client_test.go new file mode 100644 index 000000000..19f3e4680 --- /dev/null +++ b/core/internal/qsipc/client_test.go @@ -0,0 +1,58 @@ +package qsipc + +import ( + "bytes" + "encoding/hex" + "errors" + "testing" +) + +func TestWriteCall(t *testing.T) { + var got bytes.Buffer + if err := writeCall(&got, "spotlight", "toggle", []string{"true", "é"}); err != nil { + t.Fatal(err) + } + want, err := hex.DecodeString("030000001200730070006f0074006c00690067006800740000000c0074006f00670067006c0065000000020000000800740072007500650000000200e9") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got.Bytes(), want) { + t.Fatalf("unexpected wire bytes:\ngot %x\nwant %x", got.Bytes(), want) + } +} + +func TestReadResponse(t *testing.T) { + data, err := hex.DecodeString("050000000018007b0022006f006b0022003a00200074007200750065007d") + if err != nil { + t.Fatal(err) + } + value, isVoid, err := readResponse(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if isVoid || value != `{"ok": true}` { + t.Fatalf("unexpected response: value=%q void=%v", value, isVoid) + } +} + +func TestReadResponseErrors(t *testing.T) { + tests := []struct { + name string + index byte + want error + }{ + {name: "not ready", index: responseNotReady, want: ErrNotReady}, + {name: "target not found", index: responseTargetNotFound, want: ErrTargetNotFound}, + {name: "function not found", index: responseFunctionNotFound, want: ErrFunctionNotFound}, + {name: "argument mismatch", index: responseArgumentMismatch, want: ErrArgumentMismatch}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := readResponse(bytes.NewReader([]byte{tt.index})) + if !errors.Is(err, tt.want) { + t.Fatalf("readResponse() error = %v, want %v", err, tt.want) + } + }) + } +}