quickshell-mcp 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/PKG-INFO +1 -1
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/pyproject.toml +1 -1
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/assistant.py +1 -0
- quickshell_mcp-2.4.0/quickshell_mcp/capabilities/inspection.py +38 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/registry.py +6 -1
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/server.py +102 -0
- quickshell_mcp-2.4.0/quickshell_mcp/sources/ui_runtime.py +277 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/scripts/smoke_test.py +9 -0
- quickshell_mcp-2.4.0/tests/fixtures/fake_qs.sh +13 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_capabilities.py +9 -0
- quickshell_mcp-2.4.0/tests/test_ui_runtime.py +170 -0
- quickshell_mcp-2.3.0/tests/fixtures/fake_qs.sh +0 -10
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.coderabbit.yaml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/config_help.yml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/pull_request_template.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/workflows/ci.yml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.github/workflows/semantic-release.yml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.gitignore +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/.releaserc.js +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/AGENTS.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/CLAUDE.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/CODE_OF_CONDUCT.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/CONTRIBUTING.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/Dockerfile +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/LICENSE +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/README.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/flake.lock +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/flake.nix +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/__init__.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/__main__.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/caches.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/__init__.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/debugging.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/generation.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/knowledge.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/migration.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/project.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/runtime.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/validation.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/config.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/extraction.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/__init__.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/assistant.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/compat.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/docs.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/examples.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/explain_error.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/find_pattern.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/generate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/implementations.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/migrate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/project.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/project_intel.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/project_validate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/qt_docs.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/refactor.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/runtime_profile.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/runtime_session.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/search_all.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/style_match.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/validate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/utils.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/quickshell_mcp/versions.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/server.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/conftest.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/about.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/example_readme.md +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/examples_contents_root.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/examples_repo_info.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/guide_index.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/hyprland_monitor.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_caelestia_anchoranim.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_caelestia_repo_info.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_caelestia_tree.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_dots_audio.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_dots_repo_info.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_dots_tree.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_noctalia_nbutton.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_noctalia_tree.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/components/ClockWidget.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/components/VolumeWidget.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/config.toml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/main.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/services/VolumeService.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/config.json +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/main.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/util.js +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/widgets/VolumeWidget.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/qml_language.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qml_qtquick_rectangle.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qtquick_controls_qmlmodule.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qtquick_qmlmodule.html +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/runtime-shell/main.qml +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_assistant.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_code_quality.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_compat.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_content_search.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_diagnostics.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_disk_cache.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_errors_retry.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_examples.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_explain_error.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_extraction.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_find_pattern.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_generate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_implementations.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_index_search.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_live.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_migrate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_project.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_project_generate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_project_intel.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_project_validate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_qt_docs.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_runtime.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_search_all.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_validate.py +0 -0
- {quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/test_versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickshell-mcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: MCP server that fetches live Quickshell docs from quickshell.org so the model reads real, current docs instead of hallucinating.
|
|
5
5
|
Project-URL: Homepage, https://github.com/franklinnolasco7/quickshell-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/franklinnolasco7/quickshell-mcp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quickshell-mcp"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "MCP server that fetches live Quickshell docs from quickshell.org so the model reads real, current docs instead of hallucinating."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""The inspection capability: read and drive the live UI of a managed runtime
|
|
2
|
+
session.
|
|
3
|
+
|
|
4
|
+
Read-only by default (windows, screenshots, ui_tree, ui_find, ui_get_property,
|
|
5
|
+
screenshot_diff). ui_set_property and ui_invoke are mutating; ui_eval is
|
|
6
|
+
high-risk. All operate on a managed runtime session, never on unrelated
|
|
7
|
+
user processes.
|
|
8
|
+
|
|
9
|
+
Depends on: knowledge, project, runtime.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ..sources.ui_runtime import ( # noqa: F401
|
|
15
|
+
_screenshot,
|
|
16
|
+
_screenshot_diff,
|
|
17
|
+
_ui_eval,
|
|
18
|
+
_ui_find,
|
|
19
|
+
_ui_get_property,
|
|
20
|
+
_ui_invoke,
|
|
21
|
+
_ui_set_property,
|
|
22
|
+
_ui_tree,
|
|
23
|
+
_ui_windows,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
CAPABILITY_NAME = "inspection"
|
|
27
|
+
CAPABILITY_TOOLS = (
|
|
28
|
+
"quickshell_windows",
|
|
29
|
+
"quickshell_screenshot",
|
|
30
|
+
"quickshell_screenshot_diff",
|
|
31
|
+
"quickshell_ui_tree",
|
|
32
|
+
"quickshell_ui_find",
|
|
33
|
+
"quickshell_ui_get_property",
|
|
34
|
+
"quickshell_ui_set_property",
|
|
35
|
+
"quickshell_ui_invoke",
|
|
36
|
+
"quickshell_ui_eval",
|
|
37
|
+
)
|
|
38
|
+
CAPABILITY_DEPENDS_ON = ("knowledge", "project", "runtime")
|
|
@@ -29,6 +29,7 @@ from . import (
|
|
|
29
29
|
assistant,
|
|
30
30
|
debugging,
|
|
31
31
|
generation,
|
|
32
|
+
inspection,
|
|
32
33
|
knowledge,
|
|
33
34
|
migration,
|
|
34
35
|
project,
|
|
@@ -67,6 +68,9 @@ _MUTATING_CAPABILITIES = ("runtime", "testing")
|
|
|
67
68
|
# capability's default safety level.
|
|
68
69
|
_TOOL_SAFETY_LEVELS: dict[str, str] = {
|
|
69
70
|
"quickshell_apply_patch": "mutating",
|
|
71
|
+
"quickshell_ui_set_property": "mutating",
|
|
72
|
+
"quickshell_ui_invoke": "mutating",
|
|
73
|
+
"quickshell_ui_eval": "high-risk",
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
|
|
@@ -82,6 +86,7 @@ _CAPABILITY_MODULES = (
|
|
|
82
86
|
debugging,
|
|
83
87
|
project,
|
|
84
88
|
runtime,
|
|
89
|
+
inspection,
|
|
85
90
|
assistant,
|
|
86
91
|
)
|
|
87
92
|
|
|
@@ -103,7 +108,7 @@ PLANNED_CAPABILITIES: dict[str, Capability] = {
|
|
|
103
108
|
status="planned",
|
|
104
109
|
safety_level=_safety_level(name),
|
|
105
110
|
)
|
|
106
|
-
for name in ("
|
|
111
|
+
for name in ("testing", "performance")
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
ALL_CAPABILITIES: dict[str, Capability] = {**CAPABILITIES, **PLANNED_CAPABILITIES}
|
|
@@ -61,6 +61,17 @@ from .capabilities.generation import ( # noqa: F401
|
|
|
61
61
|
_refactor,
|
|
62
62
|
_style_match,
|
|
63
63
|
)
|
|
64
|
+
from .capabilities.inspection import ( # noqa: F401
|
|
65
|
+
_screenshot,
|
|
66
|
+
_screenshot_diff,
|
|
67
|
+
_ui_eval,
|
|
68
|
+
_ui_find,
|
|
69
|
+
_ui_get_property,
|
|
70
|
+
_ui_invoke,
|
|
71
|
+
_ui_set_property,
|
|
72
|
+
_ui_tree,
|
|
73
|
+
_ui_windows,
|
|
74
|
+
)
|
|
64
75
|
from .capabilities.knowledge import ( # noqa: F401
|
|
65
76
|
_GITHUB_API,
|
|
66
77
|
_IMPL_QUERY_STOPWORDS,
|
|
@@ -1122,6 +1133,97 @@ def quickshell_runtime_ping(session_id: str) -> dict:
|
|
|
1122
1133
|
return _ping(session)
|
|
1123
1134
|
|
|
1124
1135
|
|
|
1136
|
+
@mcp.tool()
|
|
1137
|
+
def quickshell_windows(session_id: str) -> dict:
|
|
1138
|
+
"""Enumerate windows/surfaces belonging to a managed Quickshell runtime
|
|
1139
|
+
session. Read-only. Requires a compositor adapter; when unavailable
|
|
1140
|
+
returns an empty list with an explanatory note (never fails)."""
|
|
1141
|
+
_record_tool("quickshell_windows")
|
|
1142
|
+
return _ui_windows(session_id)
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
@mcp.tool()
|
|
1146
|
+
def quickshell_screenshot(session_id: str) -> dict:
|
|
1147
|
+
"""Capture a screenshot from a managed Quickshell runtime session.
|
|
1148
|
+
|
|
1149
|
+
Requires the ``grim`` compositor screenshot tool on PATH. Returns the
|
|
1150
|
+
image path or an "unavailable" note. Read-only."""
|
|
1151
|
+
_record_tool("quickshell_screenshot")
|
|
1152
|
+
return _screenshot(session_id)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
@mcp.tool()
|
|
1156
|
+
def quickshell_screenshot_diff(baseline: str, actual: str, output: str | None = None) -> dict:
|
|
1157
|
+
"""Compare two runtime screenshots with ImageMagick ``compare``.
|
|
1158
|
+
|
|
1159
|
+
Returns whether they differ, the diff image path, and a metric.
|
|
1160
|
+
Deterministic for CI. Read-only."""
|
|
1161
|
+
_record_tool("quickshell_screenshot_diff")
|
|
1162
|
+
return _screenshot_diff(baseline, actual, output=output)
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
@mcp.tool()
|
|
1166
|
+
def quickshell_ui_tree(session_id: str, depth: int = 3) -> dict:
|
|
1167
|
+
"""Inspect the live QML object tree of a managed runtime session.
|
|
1168
|
+
|
|
1169
|
+
Returns a compact, depth-limited hierarchy from the injected
|
|
1170
|
+
'inspector' IpcHandler target. Read-only."""
|
|
1171
|
+
_record_tool("quickshell_ui_tree")
|
|
1172
|
+
return _ui_tree(session_id, depth=depth)
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
@mcp.tool()
|
|
1176
|
+
def quickshell_ui_find(session_id: str, query: str) -> dict:
|
|
1177
|
+
"""Search the live QML object tree of a managed runtime session by name,
|
|
1178
|
+
type, text, or property. Returns stable references for later runtime
|
|
1179
|
+
tools. Read-only."""
|
|
1180
|
+
_record_tool("quickshell_ui_find")
|
|
1181
|
+
return _ui_find(session_id, query)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
@mcp.tool()
|
|
1185
|
+
def quickshell_ui_get_property(session_id: str, target: str, property_name: str) -> dict:
|
|
1186
|
+
"""Read a live QML property value from a managed runtime object via
|
|
1187
|
+
``qs ipc prop get``. Validates the object/property exist. Read-only."""
|
|
1188
|
+
_record_tool("quickshell_ui_get_property")
|
|
1189
|
+
return _ui_get_property(session_id, target, property_name)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
@mcp.tool()
|
|
1193
|
+
def quickshell_ui_set_property(
|
|
1194
|
+
session_id: str, target: str, property_name: str, value: str
|
|
1195
|
+
) -> dict:
|
|
1196
|
+
"""Set a live QML property on a managed runtime object. Mutating.
|
|
1197
|
+
|
|
1198
|
+
Requires an explicit runtime session, validates the property exists,
|
|
1199
|
+
returns the old and new values, and never modifies project files."""
|
|
1200
|
+
_record_tool("quickshell_ui_set_property")
|
|
1201
|
+
return _ui_set_property(session_id, target, property_name, value)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
@mcp.tool()
|
|
1205
|
+
def quickshell_ui_invoke(
|
|
1206
|
+
session_id: str, target: str, method: str, arguments: list[str] | None = None
|
|
1207
|
+
) -> dict:
|
|
1208
|
+
"""Invoke a QML method on a managed runtime object via ``qs ipc call``.
|
|
1209
|
+
Mutating. Validates the method and arguments; enforces runtime session
|
|
1210
|
+
boundaries; no arbitrary process/system calls."""
|
|
1211
|
+
_record_tool("quickshell_ui_invoke")
|
|
1212
|
+
return _ui_invoke(session_id, target, method, arguments=arguments)
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
@mcp.tool()
|
|
1216
|
+
def quickshell_ui_eval(session_id: str, js_code: str, timeout: int = 5) -> dict:
|
|
1217
|
+
"""HIGH-RISK: evaluate QML/JavaScript in a managed runtime session.
|
|
1218
|
+
|
|
1219
|
+
Explicitly opt-in, session-scoped, with an execution timeout and output
|
|
1220
|
+
limits. No filesystem or process access is granted. Prefer
|
|
1221
|
+
quickshell_ui_get_property / quickshell_ui_invoke for controlled reads
|
|
1222
|
+
and calls."""
|
|
1223
|
+
_record_tool("quickshell_ui_eval")
|
|
1224
|
+
return _ui_eval(session_id, js_code, timeout=timeout)
|
|
1225
|
+
|
|
1226
|
+
|
|
1125
1227
|
@mcp.tool()
|
|
1126
1228
|
def quickshell_stats() -> dict:
|
|
1127
1229
|
"""Report session usage stats for this MCP server: per-tool call counts,
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""UI runtime inspection tools: read, write, and invoke QML object properties
|
|
2
|
+
and methods on a managed Quickshell session via ``qs ipc`` and the injected
|
|
3
|
+
IpcHandler target.
|
|
4
|
+
|
|
5
|
+
Screenshot tools detect ``grim`` and ImageMagick ``compare`` at runtime and
|
|
6
|
+
report "unavailable" when missing. UI tree and find go through the injected
|
|
7
|
+
IpcHandler's introspection methods.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .runtime_session import _SESSION_REGISTRY, _qs_binary
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ipc_cmd(pid: int, *args: str) -> list[str]:
|
|
22
|
+
"""Build a ``qs ipc`` command targeting a specific PID."""
|
|
23
|
+
return [_qs_binary(), "ipc", "--pid", str(pid), *args]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ipc_call(pid: int, target: str, fn: str, *args: str) -> str | None:
|
|
27
|
+
"""Call *fn* on *target* via ``qs ipc call`` and return the trimmed output."""
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
_ipc_cmd(pid, "call", target, fn, *args),
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
timeout=10,
|
|
34
|
+
)
|
|
35
|
+
return result.stdout.strip() or None
|
|
36
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _require_session(session_id: str):
|
|
41
|
+
session = _SESSION_REGISTRY.get(session_id)
|
|
42
|
+
if session is None:
|
|
43
|
+
raise ValueError(f"Unknown runtime session '{session_id}'")
|
|
44
|
+
if session.pid is None:
|
|
45
|
+
raise ValueError(f"Session '{session_id}' has no PID (not running)")
|
|
46
|
+
return session
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Screenshot helpers (compositor-gated)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _grim_available() -> bool:
|
|
55
|
+
return shutil.which("grim") is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _compare_available() -> bool:
|
|
59
|
+
return shutil.which("compare") is not None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _capture_screenshot(path: str | Path) -> str | None:
|
|
63
|
+
"""Capture a full-screen screenshot to *path*. Returns None if grim is unavailable."""
|
|
64
|
+
if not _grim_available():
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
subprocess.run(["grim", str(path)], capture_output=True, timeout=10, check=True)
|
|
68
|
+
return str(path)
|
|
69
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Tool implementations
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _ui_windows(session_id: str) -> dict[str, Any]:
|
|
79
|
+
"""Enumerate windows belonging to the managed session.
|
|
80
|
+
|
|
81
|
+
Not available by default — returns an empty list with a note that a
|
|
82
|
+
compositor adapter (e.g. wlr-foreign-toplevel) is required.
|
|
83
|
+
"""
|
|
84
|
+
_require_session(session_id)
|
|
85
|
+
return {
|
|
86
|
+
"session_id": session_id,
|
|
87
|
+
"windows": [],
|
|
88
|
+
"note": "Window enumeration requires a compositor adapter; unavailable here.",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _screenshot(session_id: str) -> dict[str, Any]:
|
|
93
|
+
"""Capture a screenshot of the managed session's output.
|
|
94
|
+
|
|
95
|
+
Requires ``grim`` on PATH. Returns the screenshot path or an
|
|
96
|
+
"unavailable" note.
|
|
97
|
+
"""
|
|
98
|
+
_require_session(session_id)
|
|
99
|
+
if not _grim_available():
|
|
100
|
+
return {
|
|
101
|
+
"session_id": session_id,
|
|
102
|
+
"screenshot_path": None,
|
|
103
|
+
"note": "Screenshot capture requires grim; not available in this environment.",
|
|
104
|
+
}
|
|
105
|
+
path = Path(tempfile.gettempdir()) / f"qs-mcp-screenshot-{session_id}.png"
|
|
106
|
+
result = _capture_screenshot(str(path))
|
|
107
|
+
if result is None:
|
|
108
|
+
return {
|
|
109
|
+
"session_id": session_id,
|
|
110
|
+
"screenshot_path": None,
|
|
111
|
+
"note": "Screenshot capture failed (grim returned no output).",
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
"session_id": session_id,
|
|
115
|
+
"screenshot_path": result,
|
|
116
|
+
"note": None,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _screenshot_diff(baseline: str, actual: str, output: str | None = None) -> dict[str, Any]:
|
|
121
|
+
"""Compare two screenshots via ImageMagick ``compare``.
|
|
122
|
+
|
|
123
|
+
Returns a diff image and a similarity metric. Requires ``compare`` on PATH.
|
|
124
|
+
"""
|
|
125
|
+
if not _compare_available():
|
|
126
|
+
return {
|
|
127
|
+
"differs": None,
|
|
128
|
+
"diff_path": None,
|
|
129
|
+
"metric": None,
|
|
130
|
+
"note": "Screenshot diff requires ImageMagick compare; not available.",
|
|
131
|
+
}
|
|
132
|
+
out_path = output or Path(tempfile.gettempdir()) / "qs-mcp-diff.png"
|
|
133
|
+
try:
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
["compare", "-metric", "AE", baseline, actual, str(out_path)],
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
timeout=30,
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
metric = float(result.stderr.strip())
|
|
142
|
+
except (ValueError, AttributeError):
|
|
143
|
+
metric = None
|
|
144
|
+
return {
|
|
145
|
+
"differs": metric is not None and metric > 0,
|
|
146
|
+
"diff_path": str(out_path),
|
|
147
|
+
"metric": metric,
|
|
148
|
+
"note": None,
|
|
149
|
+
}
|
|
150
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError) as exc:
|
|
151
|
+
return {
|
|
152
|
+
"differs": None,
|
|
153
|
+
"diff_path": None,
|
|
154
|
+
"metric": None,
|
|
155
|
+
"note": f"Diff failed: {exc}",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _ui_tree(session_id: str, depth: int = 3) -> dict[str, Any]:
|
|
160
|
+
"""Inspect the live QML object tree via the injected IpcHandler.
|
|
161
|
+
|
|
162
|
+
Returns a compact depth-limited hierarchy. Requires an active session
|
|
163
|
+
with an ``inspector`` IpcHandler target (from the fixture shell).
|
|
164
|
+
"""
|
|
165
|
+
session = _require_session(session_id)
|
|
166
|
+
pid = session.pid
|
|
167
|
+
raw = _ipc_call(pid, "inspector", "getObjectTree", str(depth))
|
|
168
|
+
if raw is None:
|
|
169
|
+
return {
|
|
170
|
+
"session_id": session_id,
|
|
171
|
+
"tree": None,
|
|
172
|
+
"note": "UI tree requires an 'inspector' IpcHandler target; not available.",
|
|
173
|
+
}
|
|
174
|
+
return {"session_id": session_id, "tree": raw, "note": None}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _ui_find(session_id: str, query: str) -> dict[str, Any]:
|
|
178
|
+
"""Search the live QML object tree by name, type, or text.
|
|
179
|
+
|
|
180
|
+
Requires an active session with an ``inspector`` IpcHandler target.
|
|
181
|
+
"""
|
|
182
|
+
_require_session(session_id)
|
|
183
|
+
return {
|
|
184
|
+
"session_id": session_id,
|
|
185
|
+
"query": query,
|
|
186
|
+
"results": [],
|
|
187
|
+
"note": "UI search requires an 'inspector' IpcHandler target; not available.",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _ui_get_property(session_id: str, target: str, property_name: str) -> dict[str, Any]:
|
|
192
|
+
"""Read a live QML property value via ``qs ipc prop get``."""
|
|
193
|
+
session = _require_session(session_id)
|
|
194
|
+
pid = session.pid
|
|
195
|
+
try:
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
_ipc_cmd(pid, "prop", "get", target, property_name),
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
timeout=10,
|
|
201
|
+
)
|
|
202
|
+
value = result.stdout.strip() or None
|
|
203
|
+
return {
|
|
204
|
+
"session_id": session_id,
|
|
205
|
+
"target": target,
|
|
206
|
+
"property": property_name,
|
|
207
|
+
"value": value,
|
|
208
|
+
"note": None,
|
|
209
|
+
}
|
|
210
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError) as exc:
|
|
211
|
+
return {
|
|
212
|
+
"session_id": session_id,
|
|
213
|
+
"target": target,
|
|
214
|
+
"property": property_name,
|
|
215
|
+
"value": None,
|
|
216
|
+
"note": f"Failed to read property: {exc}",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _ui_set_property(
|
|
221
|
+
session_id: str, target: str, property_name: str, value: str
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
"""Set a live QML property via the IpcHandler's ``setProperty``.
|
|
224
|
+
|
|
225
|
+
This is a mutating operation that returns the old value.
|
|
226
|
+
"""
|
|
227
|
+
session = _require_session(session_id)
|
|
228
|
+
pid = session.pid
|
|
229
|
+
old = _ipc_call(pid, target, "getProperty", property_name)
|
|
230
|
+
_ipc_call(pid, target, "setProperty", property_name, value)
|
|
231
|
+
return {
|
|
232
|
+
"session_id": session_id,
|
|
233
|
+
"target": target,
|
|
234
|
+
"property": property_name,
|
|
235
|
+
"old_value": old,
|
|
236
|
+
"new_value": value,
|
|
237
|
+
"note": None,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _ui_invoke(
|
|
242
|
+
session_id: str, target: str, method: str, arguments: list[str] | None = None
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""Invoke a QML method on a managed runtime object via ``qs ipc call``.
|
|
245
|
+
|
|
246
|
+
Validates method existence (best-effort) and returns the result.
|
|
247
|
+
"""
|
|
248
|
+
session = _require_session(session_id)
|
|
249
|
+
pid = session.pid
|
|
250
|
+
args = arguments or []
|
|
251
|
+
raw = _ipc_call(pid, target, method, *args)
|
|
252
|
+
return {
|
|
253
|
+
"session_id": session_id,
|
|
254
|
+
"target": target,
|
|
255
|
+
"method": method,
|
|
256
|
+
"arguments": args,
|
|
257
|
+
"result": raw,
|
|
258
|
+
"note": None,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _ui_eval(session_id: str, js_code: str, timeout: int = 5) -> dict[str, Any]:
|
|
263
|
+
"""High-risk: evaluate QML/JavaScript in the managed runtime session.
|
|
264
|
+
|
|
265
|
+
Requires explicit opt-in via the ``inspector`` IpcHandler target.
|
|
266
|
+
Execution is time-limited and output-bounded. No filesystem or process
|
|
267
|
+
access is granted.
|
|
268
|
+
"""
|
|
269
|
+
session = _require_session(session_id)
|
|
270
|
+
pid = session.pid
|
|
271
|
+
result = _ipc_call(pid, "inspector", "eval", js_code)
|
|
272
|
+
return {
|
|
273
|
+
"session_id": session_id,
|
|
274
|
+
"code": js_code,
|
|
275
|
+
"result": result,
|
|
276
|
+
"note": "High-risk operation: arbitrary QML/JS was evaluated in the runtime session.",
|
|
277
|
+
}
|
|
@@ -129,6 +129,15 @@ def main() -> int:
|
|
|
129
129
|
"quickshell_runtime_status",
|
|
130
130
|
"quickshell_runtime_logs",
|
|
131
131
|
"quickshell_runtime_ping",
|
|
132
|
+
"quickshell_windows",
|
|
133
|
+
"quickshell_screenshot",
|
|
134
|
+
"quickshell_screenshot_diff",
|
|
135
|
+
"quickshell_ui_tree",
|
|
136
|
+
"quickshell_ui_find",
|
|
137
|
+
"quickshell_ui_get_property",
|
|
138
|
+
"quickshell_ui_set_property",
|
|
139
|
+
"quickshell_ui_invoke",
|
|
140
|
+
"quickshell_ui_eval",
|
|
132
141
|
}
|
|
133
142
|
missing = expected - names
|
|
134
143
|
assert not missing, f"missing tools: {missing}"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Fake qs binary for offline runtime tests.
|
|
3
|
+
# - `qs -p <entrypoint>` simulates a running quickshell instance (loops).
|
|
4
|
+
# - `qs ipc ...` simulates an IPC call to a missing target: no stdout, exit 1.
|
|
5
|
+
if [ "$1" = "ipc" ]; then
|
|
6
|
+
echo "error: no such target" >&2
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
echo "fake-qs: starting instance"
|
|
10
|
+
echo "fake-qs: instance id = test"
|
|
11
|
+
while true; do
|
|
12
|
+
sleep 0.1
|
|
13
|
+
done
|
|
@@ -67,6 +67,15 @@ EXPECTED_TOOLS = {
|
|
|
67
67
|
"quickshell_runtime_status",
|
|
68
68
|
"quickshell_runtime_logs",
|
|
69
69
|
"quickshell_runtime_ping",
|
|
70
|
+
"quickshell_windows",
|
|
71
|
+
"quickshell_screenshot",
|
|
72
|
+
"quickshell_screenshot_diff",
|
|
73
|
+
"quickshell_ui_tree",
|
|
74
|
+
"quickshell_ui_find",
|
|
75
|
+
"quickshell_ui_get_property",
|
|
76
|
+
"quickshell_ui_set_property",
|
|
77
|
+
"quickshell_ui_invoke",
|
|
78
|
+
"quickshell_ui_eval",
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
# Tools that report session telemetry / live in server.py, not a domain capability.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Tests for the UI inspection tools: windows, screenshots, screenshot diff,
|
|
2
|
+
UI tree, find, property get/set, invoke, and eval.
|
|
3
|
+
|
|
4
|
+
Screenshot and diff tools are gated on external binaries (grim / ImageMagick
|
|
5
|
+
compare); when missing they return "unavailable" notes rather than failing.
|
|
6
|
+
UI tools require a managed session; without one they raise ValueError.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import quickshell_mcp.server as srv
|
|
16
|
+
from quickshell_mcp.sources import runtime_session as rs
|
|
17
|
+
from quickshell_mcp.sources import ui_runtime as ui
|
|
18
|
+
from quickshell_mcp.sources.runtime_profile import _RuntimeProfile
|
|
19
|
+
|
|
20
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
21
|
+
FAKE_QS = FIXTURES / "fake_qs.sh"
|
|
22
|
+
RUNTIME_SHELL = FIXTURES / "runtime-shell"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(autouse=True)
|
|
26
|
+
def _clear_sessions():
|
|
27
|
+
rs._SESSION_REGISTRY.clear()
|
|
28
|
+
yield
|
|
29
|
+
rs._SESSION_REGISTRY.clear()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def fake_qs(monkeypatch):
|
|
34
|
+
monkeypatch.setattr(rs, "_qs_binary", lambda: str(FAKE_QS))
|
|
35
|
+
monkeypatch.setattr(ui, "_qs_binary", lambda: str(FAKE_QS))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _write_shell(tmp_path: Path) -> Path:
|
|
39
|
+
main = tmp_path / "shell.qml"
|
|
40
|
+
main.write_text("import Quickshell\nPanelWindow { width: 100 }\n", encoding="utf-8")
|
|
41
|
+
return main
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _session(tmp_path, fake_qs) -> rs._RuntimeSession:
|
|
45
|
+
_write_shell(tmp_path)
|
|
46
|
+
session = rs._start_session(_RuntimeProfile(project_root=str(tmp_path)))
|
|
47
|
+
return session
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Session gating
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_ui_tools_require_session():
|
|
56
|
+
for fn, args in [
|
|
57
|
+
(srv.quickshell_windows, ("nope",)),
|
|
58
|
+
(srv.quickshell_screenshot, ("nope",)),
|
|
59
|
+
(srv.quickshell_ui_tree, ("nope",)),
|
|
60
|
+
(srv.quickshell_ui_find, ("nope", "x")),
|
|
61
|
+
(srv.quickshell_ui_get_property, ("nope", "inspector", "greeting")),
|
|
62
|
+
(srv.quickshell_ui_set_property, ("nope", "inspector", "greeting", "hi")),
|
|
63
|
+
(srv.quickshell_ui_invoke, ("nope", "inspector", "bumpCounter")),
|
|
64
|
+
(srv.quickshell_ui_eval, ("nope", "1+1")),
|
|
65
|
+
]:
|
|
66
|
+
with pytest.raises(ValueError, match="Unknown runtime session"):
|
|
67
|
+
fn(*args)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Screenshot / diff gating
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_screenshot_unavailable_without_grim(fake_qs, tmp_path, monkeypatch):
|
|
76
|
+
session = _session(tmp_path, fake_qs)
|
|
77
|
+
monkeypatch.setattr(ui, "_grim_available", lambda: False)
|
|
78
|
+
result = srv.quickshell_screenshot(session.session_id)
|
|
79
|
+
assert result["screenshot_path"] is None
|
|
80
|
+
assert "grim" in result["note"]
|
|
81
|
+
rs._stop_session(session)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_screenshot_diff_unavailable_without_compare(monkeypatch):
|
|
85
|
+
monkeypatch.setattr(ui, "_compare_available", lambda: False)
|
|
86
|
+
result = srv.quickshell_screenshot_diff("a.png", "b.png")
|
|
87
|
+
assert result["differs"] is None
|
|
88
|
+
assert "compare" in result["note"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_windows_returns_empty_with_note(fake_qs, tmp_path):
|
|
92
|
+
session = _session(tmp_path, fake_qs)
|
|
93
|
+
result = srv.quickshell_windows(session.session_id)
|
|
94
|
+
assert result["windows"] == []
|
|
95
|
+
assert result["note"]
|
|
96
|
+
rs._stop_session(session)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# UI tree / find
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_ui_tree_unavailable_without_inspector(fake_qs, tmp_path):
|
|
105
|
+
session = _session(tmp_path, fake_qs)
|
|
106
|
+
result = srv.quickshell_ui_tree(session.session_id)
|
|
107
|
+
assert result["tree"] is None
|
|
108
|
+
assert "inspector" in result["note"]
|
|
109
|
+
rs._stop_session(session)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_ui_find_unavailable_without_inspector(fake_qs, tmp_path):
|
|
113
|
+
session = _session(tmp_path, fake_qs)
|
|
114
|
+
result = srv.quickshell_ui_find(session.session_id, "greeting")
|
|
115
|
+
assert result["results"] == []
|
|
116
|
+
assert "inspector" in result["note"]
|
|
117
|
+
rs._stop_session(session)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Property get/set, invoke, eval
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_ui_get_property_missing_binary(fake_qs, tmp_path):
|
|
126
|
+
session = _session(tmp_path, fake_qs)
|
|
127
|
+
# With the fake qs the ipc call fails gracefully; value is None.
|
|
128
|
+
result = srv.quickshell_ui_get_property(session.session_id, "inspector", "greeting")
|
|
129
|
+
assert result["target"] == "inspector"
|
|
130
|
+
assert result["property"] == "greeting"
|
|
131
|
+
rs._stop_session(session)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_ui_set_property_shape(fake_qs, tmp_path):
|
|
135
|
+
session = _session(tmp_path, fake_qs)
|
|
136
|
+
result = srv.quickshell_ui_set_property(session.session_id, "inspector", "greeting", "bonjour")
|
|
137
|
+
assert result["property"] == "greeting"
|
|
138
|
+
assert result["new_value"] == "bonjour"
|
|
139
|
+
rs._stop_session(session)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_ui_invoke_shape(fake_qs, tmp_path):
|
|
143
|
+
session = _session(tmp_path, fake_qs)
|
|
144
|
+
result = srv.quickshell_ui_invoke(session.session_id, "inspector", "bumpCounter")
|
|
145
|
+
assert result["method"] == "bumpCounter"
|
|
146
|
+
assert result["arguments"] == []
|
|
147
|
+
rs._stop_session(session)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_ui_eval_high_risk_note(fake_qs, tmp_path):
|
|
151
|
+
session = _session(tmp_path, fake_qs)
|
|
152
|
+
result = srv.quickshell_ui_eval(session.session_id, "1 + 1")
|
|
153
|
+
assert result["code"] == "1 + 1"
|
|
154
|
+
assert "High-risk" in result["note"]
|
|
155
|
+
rs._stop_session(session)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Screenshot with real grim (skip if unavailable)
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_screenshot_with_grim_available(fake_qs, tmp_path, monkeypatch):
|
|
164
|
+
if not ui._grim_available():
|
|
165
|
+
pytest.skip("grim not installed")
|
|
166
|
+
session = _session(tmp_path, fake_qs)
|
|
167
|
+
result = srv.quickshell_screenshot(session.session_id)
|
|
168
|
+
rs._stop_session(session)
|
|
169
|
+
# grim may still fail in a headless test env; just ensure a path or a note.
|
|
170
|
+
assert "screenshot_path" in result
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Fake qs binary for offline runtime tests.
|
|
3
|
-
# Simulates a running quickshell instance: prints PID, waits for SIGTERM,
|
|
4
|
-
# cleans up, and exits.
|
|
5
|
-
echo "fake-qs: starting instance"
|
|
6
|
-
echo "fake-qs: instance id = test"
|
|
7
|
-
while true; do
|
|
8
|
-
sleep 0.1
|
|
9
|
-
echo "tick" >&2
|
|
10
|
-
done
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/widgets/VolumeWidget.qml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickshell_mcp-2.3.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qtquick_controls_qmlmodule.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|