main
sh 459 lines 12.8 KB
Raw
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 SESSION="${A0_DESKTOP_SESSION:-agent-zero-desktop}"
5 BASE_DIR="${A0_BASE_DIR:-/a0}"
6 PROFILE_DIR="${A0_DESKTOP_PROFILE:-$BASE_DIR/usr/plugins/_desktop/profiles/$SESSION}"
7 MANIFEST="${A0_DESKTOP_MANIFEST:-$BASE_DIR/usr/plugins/_desktop/sessions/$SESSION.json}"
8 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 DESKTOP_STATE_HELPER="$SCRIPT_DIR/../../../helpers/desktop_state.py"
10 DESKTOP_STATE_PYTHON="${A0_DESKTOP_STATE_PYTHON:-$(command -v /usr/bin/python3 || command -v python3 || true)}"
11
12 display_from_manifest() {
13 if [ ! -f "$MANIFEST" ] || ! command -v python3 >/dev/null 2>&1; then
14 return 0
15 fi
16 python3 - "$MANIFEST" <<'PY'
17 import json
18 import sys
19
20 try:
21 with open(sys.argv[1], "r", encoding="utf-8") as handle:
22 value = json.load(handle).get("display", "")
23 except Exception:
24 value = ""
25 if value != "":
26 print(value)
27 PY
28 }
29
30 DISPLAY_VALUE="${A0_DESKTOP_DISPLAY:-$(display_from_manifest || true)}"
31 DISPLAY_VALUE="${DISPLAY_VALUE:-120}"
32 case "$DISPLAY_VALUE" in
33 :*) export DISPLAY="$DISPLAY_VALUE" ;;
34 *) export DISPLAY=":$DISPLAY_VALUE" ;;
35 esac
36
37 export XAUTHORITY="${A0_DESKTOP_XAUTHORITY:-$PROFILE_DIR/.Xauthority}"
38 export HOME="${A0_DESKTOP_HOME:-$PROFILE_DIR}"
39 export A0_DESKTOP_SESSION="$SESSION"
40 export A0_DESKTOP_MANIFEST="$MANIFEST"
41 export A0_DESKTOP_PROFILE="$PROFILE_DIR"
42 export A0_DESKTOP_DISPLAY="$DISPLAY"
43 export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
44 export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
45 export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
46 export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-XFCE}"
47
48 command_name="${1:-help}"
49 shift || true
50
51 usage() {
52 cat <<'EOF'
53 Usage: desktopctl.sh <command> [args]
54
55 Commands:
56 env Print the X11 environment used for the Desktop.
57 check Verify that xdotool can reach the Desktop display.
58 state --json [--screenshot] [--context-id ID]
59 Return structured Desktop state as JSON.
60 observe --json [--screenshot] [--context-id ID]
61 Return structured state, optionally with a fresh screenshot.
62 screenshot [PATH] [--context-id ID]
63 Capture the Desktop to PATH, or to the chat screenshot directory when context-id is set.
64 active-window Print the active window name.
65 geometry PATTERN Print the first matching visible window geometry.
66 wait-window PATTERN Wait for a visible matching window and print its id.
67 location Print the current X pointer location.
68 windows [PATTERN] List visible window names matching PATTERN.
69 focus PATTERN Focus the first visible window matching PATTERN.
70 scroll DIRECTION [UNITS] Scroll up, down, left, or right; UNITS defaults to 5 clicks.
71 drag X1 Y1 X2 Y2 Drag from X1,Y1 to X2,Y2 in Desktop coordinates.
72 right-click X Y Move and right-click at X,Y in Desktop coordinates.
73 paste-text TEXT Put TEXT on the Desktop clipboard and paste it with an app-native shortcut.
74 sequence FILE|- Run a newline-delimited command sequence in this process.
75 batch FILE|- Alias for sequence.
76 key KEY... Send one or more xdotool key names.
77 type TEXT Type text into the focused window.
78 click X Y Move and click at X,Y in Desktop coordinates.
79 dblclick X Y Move and double-click at X,Y in Desktop coordinates.
80 launch APP Launch writer, calc, impress, terminal, settings, or workdir.
81 open-path [PATH] Open PATH in Thunar, defaulting to /a0/usr/workdir.
82 calc-set-cell FILE SHEET CELL VALUE
83 Open FILE in visible Calc, set SHEET!CELL, save, and verify.
84 save Send Ctrl+S to the focused app.
85 EOF
86 }
87
88 require_xdotool() {
89 if ! command -v xdotool >/dev/null 2>&1; then
90 echo "xdotool is required for Desktop control." >&2
91 exit 2
92 fi
93 }
94
95 ensure_display() {
96 require_xdotool
97 if ! xdotool getmouselocation >/dev/null 2>&1; then
98 echo "Desktop X display is not reachable. Open the Desktop surface first." >&2
99 exit 2
100 fi
101 }
102
103 desktop_state() {
104 if [ ! -f "$DESKTOP_STATE_HELPER" ]; then
105 echo "Desktop state helper not found: $DESKTOP_STATE_HELPER" >&2
106 exit 2
107 fi
108 "$DESKTOP_STATE_PYTHON" "$DESKTOP_STATE_HELPER" "$@"
109 }
110
111 run_detached() {
112 ( "$@" >/tmp/a0-desktopctl.log 2>&1 & )
113 }
114
115 close_blocking_dialogs() {
116 require_xdotool
117 for title in "Document in Use"; do
118 window_ids="$(xdotool search --onlyvisible --name "$title" 2>/dev/null || true)"
119 printf '%s\n' "$window_ids" | while read -r window_id; do
120 [ -n "$window_id" ] && xdotool windowclose "$window_id" >/dev/null 2>&1 || true
121 done
122 done
123 }
124
125 first_window() {
126 pattern="$1"
127 xdotool search --onlyvisible --name "$pattern" 2>/dev/null | head -n 1 || true
128 }
129
130 active_window_id() {
131 xdotool getactivewindow 2>/dev/null || true
132 }
133
134 active_window_class() {
135 window_id="$(active_window_id)"
136 if [ -z "$window_id" ]; then
137 return 0
138 fi
139 if command -v xprop >/dev/null 2>&1; then
140 xprop -id "$window_id" WM_CLASS 2>/dev/null | awk -F'"' '/WM_CLASS/ { print $(NF - 1); exit }'
141 fi
142 }
143
144 active_window_class_lower() {
145 active_window_class | tr '[:upper:]' '[:lower:]'
146 }
147
148 active_window_is_terminal() {
149 window_class="$(active_window_class_lower)"
150 case "$window_class" in
151 *terminal*|xterm|uxterm|rxvt|urxvt|kitty|alacritty|wezterm|konsole)
152 return 0
153 ;;
154 *)
155 return 1
156 ;;
157 esac
158 }
159
160 paste_key_for_active_window() {
161 printf '%s\n' "${A0_DESKTOP_PASTE_KEY:-ctrl+v}"
162 }
163
164 window_geometry() {
165 window_id="$1"
166 if command -v xwininfo >/dev/null 2>&1; then
167 xwininfo -id "$window_id" 2>/dev/null | awk '
168 /Absolute upper-left X:/ { x=$4 }
169 /Absolute upper-left Y:/ { y=$4 }
170 /Width:/ { w=$2 }
171 /Height:/ { h=$2 }
172 END { if (w != "") printf "X=%s\nY=%s\nWIDTH=%s\nHEIGHT=%s\n", x, y, w, h }
173 '
174 else
175 xdotool getwindowgeometry --shell "$window_id"
176 fi
177 }
178
179 wait_window() {
180 pattern="$1"
181 timeout="${2:-15}"
182 end=$((SECONDS + timeout))
183 while [ "$SECONDS" -le "$end" ]; do
184 window_id="$(first_window "$pattern")"
185 if [ -n "$window_id" ]; then
186 printf '%s\n' "$window_id"
187 return 0
188 fi
189 sleep 0.25
190 done
191 echo "Timed out waiting for visible window: $pattern" >&2
192 return 1
193 }
194
195 scroll_desktop() {
196 direction="$1"
197 units="${2:-5}"
198 case "$direction" in
199 up) button=4 ;;
200 down) button=5 ;;
201 left) button=6 ;;
202 right) button=7 ;;
203 *)
204 echo "scroll direction must be up, down, left, or right." >&2
205 exit 2
206 ;;
207 esac
208 xdotool click --repeat "$units" "$button"
209 }
210
211 paste_text() {
212 text="$*"
213 if active_window_is_terminal; then
214 xdotool type --delay "${A0_DESKTOP_PASTE_TYPE_DELAY_MS:-${A0_DESKTOP_TYPE_DELAY_MS:-4}}" -- "$text"
215 return
216 fi
217 if command -v xclip >/dev/null 2>&1; then
218 printf '%s' "$text" | xclip -selection clipboard
219 xdotool key --clearmodifiers "$(paste_key_for_active_window)"
220 return
221 fi
222 xdotool type --delay "${A0_DESKTOP_TYPE_DELAY_MS:-1}" -- "$text"
223 }
224
225 run_sequence_line() {
226 line="$1"
227 [ -z "$line" ] && return 0
228 case "$line" in
229 \#*) return 0 ;;
230 esac
231 # shellcheck disable=SC2086
232 dispatch_command $line
233 }
234
235 run_sequence() {
236 source_file="$1"
237 if [ "$source_file" = "-" ]; then
238 while IFS= read -r line; do
239 run_sequence_line "$line"
240 done
241 return
242 fi
243 if [ ! -f "$source_file" ]; then
244 echo "sequence requires an existing FILE or - for stdin." >&2
245 exit 2
246 fi
247 while IFS= read -r line || [ -n "$line" ]; do
248 run_sequence_line "$line"
249 done < "$source_file"
250 }
251
252 launch_app() {
253 app="${1:-}"
254 soffice="${SOFFICE:-$(command -v soffice || true)}"
255 soffice="${soffice:-soffice}"
256 case "$app" in
257 writer)
258 run_detached "$soffice" --norestore --nofirststartwizard --nolockcheck "-env:UserInstallation=file://$HOME" --writer
259 ;;
260 calc|spreadsheet)
261 run_detached "$soffice" --norestore --nofirststartwizard --nolockcheck "-env:UserInstallation=file://$HOME" --calc
262 ;;
263 impress|presentation)
264 run_detached "$soffice" --norestore --nofirststartwizard --nolockcheck "-env:UserInstallation=file://$HOME" --impress
265 ;;
266 terminal)
267 run_detached xfce4-terminal --working-directory=/a0/usr/workdir
268 ;;
269 settings)
270 run_detached xfce4-settings-manager
271 ;;
272 workdir|files|file-manager)
273 run_detached thunar /a0/usr/workdir
274 ;;
275 *)
276 echo "Unknown app: ${app:-<empty>}" >&2
277 echo "Expected: writer, calc, impress, terminal, settings, or workdir." >&2
278 exit 2
279 ;;
280 esac
281 }
282
283 dispatch_command() {
284 local command_name="${1:-help}"
285 shift || true
286
287 case "$command_name" in
288 help|-h|--help)
289 usage
290 ;;
291 env)
292 printf 'export DISPLAY=%q\n' "$DISPLAY"
293 printf 'export XAUTHORITY=%q\n' "$XAUTHORITY"
294 printf 'export HOME=%q\n' "$HOME"
295 ;;
296 check)
297 ensure_display
298 xdotool getmouselocation --shell
299 ;;
300 state)
301 if [ "${1:-}" != "--json" ]; then
302 echo "state currently requires --json." >&2
303 exit 2
304 fi
305 shift
306 desktop_state state --json "$@"
307 ;;
308 observe)
309 if [ "${1:-}" != "--json" ]; then
310 echo "observe currently requires --json." >&2
311 exit 2
312 fi
313 shift
314 desktop_state observe --json "$@"
315 ;;
316 screenshot)
317 if [ "${1:-}" = "--json" ]; then
318 shift
319 desktop_state screenshot --json "$@"
320 elif [ "$#" -gt 0 ]; then
321 desktop_state screenshot "$@"
322 else
323 desktop_state screenshot
324 fi
325 ;;
326 active-window)
327 ensure_display
328 window_id="$(active_window_id)"
329 if [ -z "$window_id" ]; then
330 echo "No active window." >&2
331 exit 1
332 fi
333 xdotool getwindowname "$window_id"
334 ;;
335 geometry)
336 ensure_display
337 pattern="${1:?geometry requires a window name pattern}"
338 window_id="$(first_window "$pattern")"
339 if [ -z "$window_id" ]; then
340 echo "No visible window matched: $pattern" >&2
341 exit 1
342 fi
343 window_geometry "$window_id"
344 ;;
345 wait-window)
346 ensure_display
347 pattern="${1:?wait-window requires a window name pattern}"
348 wait_window "$pattern" "${2:-15}"
349 ;;
350 location)
351 ensure_display
352 xdotool getmouselocation --shell
353 ;;
354 windows)
355 ensure_display
356 pattern="${1:-.}"
357 xdotool search --onlyvisible --name "$pattern" getwindowname %@ 2>/dev/null || true
358 ;;
359 focus)
360 ensure_display
361 pattern="${1:?focus requires a window name pattern}"
362 window_id="$(first_window "$pattern")"
363 if [ -z "$window_id" ]; then
364 echo "No visible window matched: $pattern" >&2
365 exit 1
366 fi
367 xdotool windowactivate --sync "$window_id"
368 ;;
369 scroll)
370 ensure_display
371 scroll_desktop "${1:?scroll requires DIRECTION}" "${2:-5}"
372 ;;
373 drag)
374 ensure_display
375 x1="${1:?drag requires X1}"
376 y1="${2:?drag requires Y1}"
377 x2="${3:?drag requires X2}"
378 y2="${4:?drag requires Y2}"
379 xdotool mousemove --sync "$x1" "$y1" mousedown 1 mousemove --sync "$x2" "$y2" mouseup 1
380 ;;
381 right-click)
382 ensure_display
383 x="${1:?right-click requires X}"
384 y="${2:?right-click requires Y}"
385 xdotool mousemove --sync "$x" "$y" click 3
386 ;;
387 paste-text)
388 ensure_display
389 if [ "$#" -eq 0 ]; then
390 echo "paste-text requires TEXT." >&2
391 exit 2
392 fi
393 paste_text "$@"
394 ;;
395 sequence|batch)
396 source_file="${1:?sequence requires FILE or -}"
397 run_sequence "$source_file"
398 ;;
399 key)
400 ensure_display
401 if [ "$#" -eq 0 ]; then
402 echo "key requires at least one xdotool key name." >&2
403 exit 2
404 fi
405 xdotool key --clearmodifiers "$@"
406 ;;
407 type)
408 ensure_display
409 text="$*"
410 xdotool type --delay "${A0_DESKTOP_TYPE_DELAY_MS:-1}" -- "$text"
411 ;;
412 click)
413 ensure_display
414 x="${1:?click requires X}"
415 y="${2:?click requires Y}"
416 xdotool mousemove --sync "$x" "$y" click 1
417 ;;
418 dblclick)
419 ensure_display
420 x="${1:?dblclick requires X}"
421 y="${2:?dblclick requires Y}"
422 xdotool mousemove --sync "$x" "$y" click --repeat 2 --delay "${A0_DESKTOP_DBLCLICK_DELAY_MS:-150}" 1
423 ;;
424 launch)
425 ensure_display
426 launch_app "${1:-}"
427 ;;
428 open-path)
429 ensure_display
430 path="${1:-/a0/usr/workdir}"
431 run_detached thunar "$path"
432 ;;
433 calc-set-cell)
434 ensure_display
435 file="${1:?calc-set-cell requires FILE}"
436 sheet="${2:?calc-set-cell requires SHEET}"
437 cell="${3:?calc-set-cell requires CELL}"
438 shift 3
439 if [ "$#" -eq 0 ]; then
440 echo "calc-set-cell requires VALUE." >&2
441 exit 2
442 fi
443 close_blocking_dialogs
444 export PYTHONPATH="${PYTHONPATH:-/usr/lib/python3/dist-packages:/usr/lib/libreoffice/program:}"
445 python3 "$SCRIPT_DIR/calc_set_cell.py" "$file" "$sheet" "$cell" "$@"
446 ;;
447 save)
448 ensure_display
449 xdotool key --clearmodifiers ctrl+s
450 ;;
451 *)
452 echo "Unknown command: $command_name" >&2
453 usage >&2
454 exit 2
455 ;;
456 esac
457 }
458
459 dispatch_command "$command_name" "$@"