@ai-ecoverse/slicc-capture 1.1.0 → 1.2.0
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.
- package/README.md +41 -19
- package/bin/sccapture +2 -0
- package/bin/screencapture +310 -0
- package/package.json +19 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ screencapture -T 3 -R 0,0,800,600 -t jpg crop.jpg
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
In SLICC, the package declares `screencapture` and `sccapture` in `slicc.commands
|
|
16
|
+
In SLICC, the package declares `screencapture` and `sccapture` in `slicc.commands` (POSIX `sh` scripts in `bin/`), so installing it puts both in `/usr/bin`. They need slicc-kernel 1.153.0 or later, for [`/dev/embedder`](#the-request) (`engines.slicc-kernel` is `>=1.153.0`; an older kernel refuses them with exit 126 and `<cmd>: @ai-ecoverse/slicc-capture@<version> needs slicc-kernel >=1.153.0; this kernel is <version>`):
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
pnpm add -g @ai-ecoverse/slicc-capture
|
|
@@ -72,33 +72,55 @@ Short options may be grouped (`-xc`), an option's value may be attached or separ
|
|
|
72
72
|
|
|
73
73
|
### In SLICC
|
|
74
74
|
|
|
75
|
-
`screencapture file` asks seven's page to capture. `getDisplayMedia` needs a user gesture, so the page first shows a prompt, `screencapture wants to capture your screen`, with **Choose…** and **Cancel**; **Choose…** opens Chrome's picker. After the user picks, the
|
|
75
|
+
`screencapture file` asks seven's page to capture, as a `capture.screen` request on `/dev/embedder` ([below](#the-request)). `getDisplayMedia` needs a user gesture, so the page first shows a prompt, `screencapture wants to capture your screen`, with **Choose…** and **Cancel**; **Choose…** opens Chrome's picker. After the user picks, the page waits for `-T` seconds, if given, takes one frame, crops it to `-R`, if given, encodes it and stops the capture. The command writes the bytes it gets back unchanged; an existing file is replaced only once the capture has arrived.
|
|
76
76
|
|
|
77
77
|
- **The file:** written as PNG, JPEG or WebP: `-t` decides, else the file's extension (`.png`, `.jpg` or `.jpeg`, `.webp`), else PNG. JPEG and WebP use quality 0.92. On success it prints `captured <n> KB to <file>`.
|
|
78
|
-
- **`-c`:** copies
|
|
79
|
-
- **`-v`:** shows the capture inline in the agent's tool result.
|
|
78
|
+
- **`-c`:** captures a PNG and copies it to the clipboard as `image/png`, with a `clipboard.write` request on `/dev/embedder`, which seven's embedder (slicc-bios) answers; [slicc-clipboard](https://github.com/ai-ecoverse/slicc-clipboard)'s `/dev/clipboard` stays text only. It prints `captured <n> KB to clipboard`. A file named with `-c` is ignored, as with macOS's `-c`, and so is `-t`. The `clipboard.write` request is sent before the prompt, so a page that doesn't answer it fails at once, without a capture.
|
|
79
|
+
- **`-v`:** shows the capture inline in the agent's tool result, by printing one line `<img:<absolute path>>` on stdout, the marker agreed in [ai-ecoverse/slicc-agent#713](https://github.com/ai-ecoverse/slicc-agent/issues/713): the agent reads that file, detects PNG, JPEG, GIF or WebP by its magic bytes, fits the image to 1600 px wide and 1 MB of base64, and replaces the line with `<path> (<mime>, <bytes> bytes)` and the image in the tool result (at most 4 per command). With a file, `-v` writes it and prints only the marker for it; without one, it writes the capture to a new `${TMPDIR:-/tmp}/screencapture-XXXXXX.<type>`, which it leaves there for the agent, and prints the marker for that; with `-c`, it copies, prints `captured <n> KB to clipboard`, and then the marker of such a file. slicc 6's `<img:data:<type>;base64,<data>>` is gone: base64 on stdout is scrubbed and cut by the output tail. The agent's side of #713 isn't shipped yet; until it is, the marker shows as the line itself. macOS's `-v` (record a video) is out of scope everywhere.
|
|
80
80
|
- **No file:** without a file, `-c` or `-v`: `screencapture: output file required (or use -c for clipboard)` and the usage to stderr, exit 2.
|
|
81
81
|
|
|
82
82
|
Errors, on stderr, from slicc 6 where they still apply:
|
|
83
83
|
|
|
84
84
|
| error | exit |
|
|
85
85
|
| --- | --- |
|
|
86
|
-
| `screencapture: user cancelled or
|
|
87
|
-
| `screencapture: timed out waiting for the picker` (no answer within 5 minutes) | 1 |
|
|
88
|
-
| `screencapture: browser APIs are unavailable in this environment` (
|
|
89
|
-
| `screencapture: screen capture is not supported in this browser` (no `getDisplayMedia`) | 1 |
|
|
90
|
-
| `screencapture: -R: rectangle outside the capture` | 1 |
|
|
91
|
-
| `screencapture:
|
|
92
|
-
| `screencapture:
|
|
86
|
+
| `screencapture: user cancelled, permission denied, or the surface is protected` (`EACCES`: **Cancel**, the picker closed, the capture refused, or the embedder refused the surface the user picked) | 1 |
|
|
87
|
+
| `screencapture: timed out waiting for the picker` (no answer within 5 minutes; the page's message) | 1 |
|
|
88
|
+
| `screencapture: browser APIs are unavailable in this environment` (`ENOTSUP` for the header: no page answers `capture.screen`) | 1 |
|
|
89
|
+
| `screencapture: screen capture is not supported in this browser` (`ENOTSUP` from the page, without a message of its own: no `getDisplayMedia`) | 1 |
|
|
90
|
+
| `screencapture: -R: rectangle outside the capture` (the page's message) | 1 |
|
|
91
|
+
| `screencapture: <message>` (any other error of the page, `EIO` or `ENOENT`: the first line of its message, else the errno's text, such as `I/O error`) | 1 |
|
|
92
|
+
| `screencapture: the page sent no image` (an empty reply) | 1 |
|
|
93
|
+
| `screencapture: interrupted` (`SIGHUP`, `SIGINT` or `SIGTERM` during the request: the command stops the reader, so the page's `signal` aborts with `interrupted`, removes its temporary files and writes no file) | `128 + n` |
|
|
94
|
+
| `screencapture: failed to write file: <file>: <message>` | 1 |
|
|
95
|
+
| `screencapture: failed to copy to clipboard: this page does not copy images (no clipboard.write)` (`ENOTSUP`: no page answers `clipboard.write`) | 1 |
|
|
96
|
+
| `screencapture: failed to copy to clipboard: <message>` (the page's message, else the errno's text) | 1 |
|
|
97
|
+
| `screencapture: /dev/embedder not available (needs slicc-kernel >= 1.153.0)` (a kernel that runs the commands without the device) | 1 |
|
|
93
98
|
| `screencapture: unknown option: <arg>`, then the usage | 2 |
|
|
94
99
|
| `screencapture: <opt>: not supported in SLICC` | 2 |
|
|
95
100
|
| `screencapture: -t <format>: not supported in SLICC` | 2 |
|
|
96
101
|
| `screencapture: -T: invalid delay: <value>`, `-R: invalid rectangle: <value>`, `-D: invalid display: <value>`, `-J: invalid style: <value>` | 2 |
|
|
97
102
|
| `screencapture: more than one file: not supported in SLICC` | 2 |
|
|
98
103
|
|
|
99
|
-
As `sccapture`, the messages start with `sccapture:`.
|
|
104
|
+
As `sccapture`, the messages start with `sccapture:`. A line of progress from the page (`progress(line)`) reaches stderr as `<cmd>: <line>` while the command waits.
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
### The request
|
|
107
|
+
|
|
108
|
+
The commands run in a kernel worker, which has no `getDisplayMedia`, so they ask seven's page over [`/dev/embedder`](https://github.com/ai-ecoverse/slicc-kernel#devembedder) (slicc-kernel 1.153.0, [#1527](https://github.com/ai-ecoverse/slicc-kernel/issues/1527)): one open file description per request, a JSON header line `{ kind, …options }`, then the body, and the reply read to end-of-file. The page side, the prompt, `getDisplayMedia`, the frame, the crop and the encoding, is a page module that slicc-bios registers for `capture.screen` and wraps with its own prompt, user activation, `selfBrowserSurface: 'exclude'` and a check of the picked surface against protected origins; it answers this contract:
|
|
109
|
+
|
|
110
|
+
- **`capture.screen`:** the header is plain JSON, with no body, and the reply is the encoded image's bytes, nothing else. The command opens the device read-write (`exec 3<>/dev/embedder`), writes the header from a child `sh` that stays until the reply is read (so the page's progress reaches stderr), and reads the reply with a `cat <&3` that it starts in the background, so that a signal to the command can stop it.
|
|
111
|
+
|
|
112
|
+
| header field | value | from |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `kind` | `"capture.screen"` | |
|
|
115
|
+
| `type` | `"image/png"`, `"image/jpeg"` or `"image/webp"` | `-t`, else the file's extension, else PNG; always PNG with `-c` |
|
|
116
|
+
| `quality` | `0.92`, only for JPEG and WebP | |
|
|
117
|
+
| `cursor` | `"always"` or `"never"`, for `getDisplayMedia`'s `cursor` | `-C` |
|
|
118
|
+
| `surface` | `"monitor"` or `"window"`, absent without one, for `displaySurface` (a hint: the user still picks) | `-m` and `-D` give `"monitor"`; `-w`, `-W` and `-J window` give `"window"`; the last one wins |
|
|
119
|
+
| `delay` | seconds as a JSON number (`3`, `0.5`), absent without `-T`; the page waits that long after the picker | `-T` |
|
|
120
|
+
| `rect` | `{ "x", "y", "width", "height" }`, whole numbers in the frame's pixels, absent without `-R`; outside the frame, the page rejects with `-R: rectangle outside the capture` | `-R` |
|
|
121
|
+
|
|
122
|
+
For example, `screencapture -C -T 2 -R 0,0,800,600 shot.jpg` sends `{"kind":"capture.screen","type":"image/jpeg","quality":0.92,"cursor":"always","delay":2,"rect":{"x":0,"y":0,"width":800,"height":600}}`. The page rejects with a `NotAllowedError` or `AbortError` for **Cancel**, a closed picker, a refused permission or a protected surface (`EACCES`); a `NotSupportedError` without `getDisplayMedia` (`ENOTSUP`); and any other error, such as a `TimeoutError` `timed out waiting for the picker` after 5 minutes, with the message the command prints (`EIO`). It aborts the capture when its `signal` does.
|
|
123
|
+
- **`clipboard.write`:** `-c` sends `{"kind":"clipboard.write","type":"image/png"}` and the PNG as the body, and reads the reply, which is empty, to end-of-file. slicc-bios answers it; with no page that does, the header write fails with `ENOTSUP`. The device is opened read-write and the reply read only after the whole body is written, so a page that answers without reading the body ([slicc-kernel#1536](https://github.com/ai-ecoverse/slicc-kernel/issues/1536)) is the only way a body over 1 MiB could block; the embedder's handler reads it.
|
|
102
124
|
|
|
103
125
|
### Options
|
|
104
126
|
|
|
@@ -109,8 +131,8 @@ Every option of macOS's `screencapture`, and what it does in SLICC and with `scc
|
|
|
109
131
|
| `-a` | don't capture attached windows | no-op | passed | no-op | no-op |
|
|
110
132
|
| `-b` | capture the Touch Bar | not supported | passed | not supported | not supported |
|
|
111
133
|
| `-B <bundleid>` | open in that app | not supported | passed | not supported | not supported |
|
|
112
|
-
| `-c` | to the clipboard |
|
|
113
|
-
| `-C` | include the cursor |
|
|
134
|
+
| `-c` | to the clipboard | a `clipboard.write` request, as `image/png` | passed | see [Linux](#linux) | `Clipboard.SetImage` |
|
|
135
|
+
| `-C` | include the cursor | `"cursor": "always"` (else `"never"`), which the browser may not honour | passed | see [Linux](#linux) | draws the cursor into the image |
|
|
114
136
|
| `-d` | show errors graphically | no-op | passed | no-op | no-op |
|
|
115
137
|
| `-D <display>` | that display (1 is main) | prefers `displaySurface: 'monitor'`; the user picks the screen | passed | not supported | screen *n*: 1 is the primary, then the others in `AllScreens` order; no such screen is `sccapture: -D <n>: no such display`, exit 1 |
|
|
116
138
|
| `-g` | record default audio input | not supported | not supported | not supported | not supported |
|
|
@@ -132,7 +154,7 @@ Every option of macOS's `screencapture`, and what it does in SLICC and with `scc
|
|
|
132
154
|
| `-T <seconds>` | wait before capturing | waits after the picker | passed | `sccapture` waits, then runs the tool | `sccapture` waits, then captures |
|
|
133
155
|
| `-u` | show the UI afterwards, file ignored | not supported | passed | not supported | not supported |
|
|
134
156
|
| `-U` | the interactive toolbar | not supported | passed | not supported | not supported |
|
|
135
|
-
| `-v` | record a video | inline in the tool result (slicc
|
|
157
|
+
| `-v` | record a video | inline in the tool result, as an `<img:<path>>` line ([slicc-agent#713](https://github.com/ai-ecoverse/slicc-agent/issues/713)) | not supported | not supported | not supported |
|
|
136
158
|
| `-V <seconds>` | record a video for that long | not supported | not supported | not supported | not supported |
|
|
137
159
|
| `-w` | window selection only | prefers `displaySurface: 'window'` | passed | see [Linux](#linux) | not supported |
|
|
138
160
|
| `-W` | start in window selection | prefers `displaySurface: 'window'` | passed | as `-w` | not supported |
|
|
@@ -201,9 +223,9 @@ npm run test:unit
|
|
|
201
223
|
```
|
|
202
224
|
|
|
203
225
|
- `npm install` also installs the git hooks into this worktree's own `node_modules/` through slicc-shared-web's `slicc-install-hooks` and the committed `.githooks/`.
|
|
204
|
-
- `npm run lint`: Biome, the no-comment check and the other [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web) checks, then `shellcheck -s sh` and `dash -n` on `install.sh
|
|
205
|
-
- `npm test`: the integration tests in `test/integration/`. `test/integration/repo.test.mjs` checks the workflow, the release config, the pinned devDependencies and what `npm pack` ships. The host tests run `host/` against fake `screencapture`, `grim`, `slurp`, `gnome-screenshot`, `import`, `scrot`, `wl-copy`, `xclip`, `xdg-open`, `powershell.exe` and `pwsh.exe` on a temporary `PATH`, and `--upgrade` against installers served from a local HTTP server, with the platform forced, so each branch is checked on any machine; only CI touches a real screen. `install.test.mjs` runs `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake `nodejs.org`, and `--upgrade` against a release served from a local directory; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`.
|
|
206
|
-
- `npm run test:unit`: the unit tests in the gitignored `test/unit/`, with coverage in `coverage/unit/lcov.info`. The pre-commit hook wants 100% coverage of the changed lines, and runs `diff-cover` through `uvx`, so install [uv](https://docs.astral.sh/uv/) once.
|
|
226
|
+
- `npm run lint`: Biome, the no-comment check and the other [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web) checks, then `shellcheck -s sh` and `dash -n` on `install.sh` and `bin/*`.
|
|
227
|
+
- `npm test`: the integration tests in `test/integration/`. `commands.test.mjs` runs `screencapture` and `sccapture` on `@ai-ecoverse/slicc-kernel/node` (the version `engines.slicc-kernel` names, pinned) with wasm-bash and wasm-coreutils, and a fake `capture.screen` and `clipboard.write` handler (`createNodeKernel({ requests })`): every option's header, the bytes written unchanged, the errnos and their messages, `-c`, `-v`, and slicc-kernel 1.152.2, which has no `/dev/embedder`. `test/integration/repo.test.mjs` checks the workflow, the release config, the pinned devDependencies and what `npm pack` ships. The host tests run `host/` against fake `screencapture`, `grim`, `slurp`, `gnome-screenshot`, `import`, `scrot`, `wl-copy`, `xclip`, `xdg-open`, `powershell.exe` and `pwsh.exe` on a temporary `PATH`, and `--upgrade` against installers served from a local HTTP server, with the platform forced, so each branch is checked on any machine; only CI touches a real screen. `install.test.mjs` runs `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake `nodejs.org`, and `--upgrade` against a release served from a local directory; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`.
|
|
228
|
+
- `npm run test:unit`: the unit tests in the gitignored `test/unit/`, with coverage in `coverage/unit/lcov.info`. `npm run test:sh` runs the `test/unit/*.sh.mjs` ones, which trace `bin/` with `set -x` in the node kernel, and writes `coverage/sh/lcov.info`. The pre-commit hook wants 100% coverage of the changed lines of both, and runs `diff-cover` through `uvx`, so install [uv](https://docs.astral.sh/uv/) once.
|
|
207
229
|
|
|
208
230
|
CI is `.github/workflows/ci.yml`, the only workflow, which runs [slicc-shared-web's pipeline](https://github.com/ai-ecoverse/slicc-shared-web#pipeline) at the devDependency's version: `npm run lint`, then `npm test`, then on a pull request the Claude and Codex reviews. The merge queue requires `ci / integration` and `ci / review`. On a push to `main`, semantic-release releases and publishes to npm through trusted publishing, with provenance from `ci.yml`, and attaches the installers.
|
|
209
231
|
|
package/bin/sccapture
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
me=${0##*/}
|
|
3
|
+
dev=/dev/embedder
|
|
4
|
+
usage() {
|
|
5
|
+
printf 'usage: %s [-acdimorvwxCW] [-t format] [-T seconds] [-R x,y,w,h] [-D display] [-J style] [file]\n' "$me"
|
|
6
|
+
}
|
|
7
|
+
say() {
|
|
8
|
+
printf '%s: %s\n' "$me" "$1" >&2
|
|
9
|
+
}
|
|
10
|
+
bad() {
|
|
11
|
+
say "$1"
|
|
12
|
+
usage >&2
|
|
13
|
+
exit 2
|
|
14
|
+
}
|
|
15
|
+
refuse() {
|
|
16
|
+
say "$1"
|
|
17
|
+
exit 2
|
|
18
|
+
}
|
|
19
|
+
die() {
|
|
20
|
+
say "$1"
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
first() {
|
|
24
|
+
[ -n "$problem" ] || problem=$1
|
|
25
|
+
}
|
|
26
|
+
num() {
|
|
27
|
+
n=$1
|
|
28
|
+
while :; do
|
|
29
|
+
case $n in
|
|
30
|
+
0[0-9]*) n=${n#0} ;;
|
|
31
|
+
*) break ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
printf '%s' "$n"
|
|
35
|
+
}
|
|
36
|
+
digits() {
|
|
37
|
+
case $1 in
|
|
38
|
+
'' | *[!0-9]*) return 1 ;;
|
|
39
|
+
esac
|
|
40
|
+
}
|
|
41
|
+
delay() {
|
|
42
|
+
case $1 in
|
|
43
|
+
'' | .* | *. | *.*.* | *[!0-9.]*) return 1 ;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
rect() {
|
|
47
|
+
case $1 in
|
|
48
|
+
*,*,*,*,*) return 1 ;;
|
|
49
|
+
*,*,*,*) ;;
|
|
50
|
+
*) return 1 ;;
|
|
51
|
+
esac
|
|
52
|
+
rx=${1%%,*}
|
|
53
|
+
rest=${1#*,}
|
|
54
|
+
ry=${rest%%,*}
|
|
55
|
+
rest=${rest#*,}
|
|
56
|
+
rw=${rest%%,*}
|
|
57
|
+
rh=${rest#*,}
|
|
58
|
+
digits "$rx" && digits "$ry" && digits "$rw" && digits "$rh" || return 1
|
|
59
|
+
[ "$(num "$rw")" != 0 ] && [ "$(num "$rh")" != 0 ]
|
|
60
|
+
}
|
|
61
|
+
format() {
|
|
62
|
+
case $1 in
|
|
63
|
+
[pP][nN][gG]) printf png ;;
|
|
64
|
+
[jJ][pP][gG] | [jJ][pP][eE][gG]) printf jpeg ;;
|
|
65
|
+
[wW][eE][bB][pP]) printf webp ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
value() {
|
|
69
|
+
case $1 in
|
|
70
|
+
T)
|
|
71
|
+
delay "$2" || first "-T: invalid delay: $2"
|
|
72
|
+
wait=$2
|
|
73
|
+
;;
|
|
74
|
+
R)
|
|
75
|
+
rect "$2" || first "-R: invalid rectangle: $2"
|
|
76
|
+
area=$2
|
|
77
|
+
;;
|
|
78
|
+
D)
|
|
79
|
+
case $2 in
|
|
80
|
+
[1-9]*) digits "$2" || first "-D: invalid display: $2" ;;
|
|
81
|
+
*) first "-D: invalid display: $2" ;;
|
|
82
|
+
esac
|
|
83
|
+
surface=monitor
|
|
84
|
+
;;
|
|
85
|
+
J)
|
|
86
|
+
case $2 in
|
|
87
|
+
window) surface=window ;;
|
|
88
|
+
selection | video) style=${style:-"-J $2: not supported in SLICC"} ;;
|
|
89
|
+
*) first "-J: invalid style: $2" ;;
|
|
90
|
+
esac
|
|
91
|
+
;;
|
|
92
|
+
t) type=$2 ;;
|
|
93
|
+
*) off=${off:-"-$1"} ;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
96
|
+
flag() {
|
|
97
|
+
case $1 in
|
|
98
|
+
c) clip=1 ;;
|
|
99
|
+
C) cursor=always ;;
|
|
100
|
+
v) view=1 ;;
|
|
101
|
+
m) surface=monitor ;;
|
|
102
|
+
w | W) surface=window ;;
|
|
103
|
+
a | d | i | o | r | x) ;;
|
|
104
|
+
*) off=${off:-"-$1"} ;;
|
|
105
|
+
esac
|
|
106
|
+
}
|
|
107
|
+
version() {
|
|
108
|
+
while IFS= read -r line; do
|
|
109
|
+
case $line in
|
|
110
|
+
*'"version": "'*)
|
|
111
|
+
line=${line#*'"version": "'}
|
|
112
|
+
printf '%s\n' "${line%%\"*}"
|
|
113
|
+
return
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
done <"${0%/*}/../package.json"
|
|
117
|
+
}
|
|
118
|
+
relay() {
|
|
119
|
+
while IFS= read -r line; do
|
|
120
|
+
case $line in
|
|
121
|
+
*": $dev: "*) printf '%s\n' "${line#*": $dev: "}" >"$work/message" ;;
|
|
122
|
+
*": printf: write error: "*) printf '%s\n' "${line##*: }" >"$work/header" ;;
|
|
123
|
+
"cat: -: "* | "cat: write error: "*) printf '%s\n' "${line##*: }" >"$work/errno" ;;
|
|
124
|
+
*) printf '%s: %s\n' "$me" "${line#*: }" >&2 ;;
|
|
125
|
+
esac
|
|
126
|
+
done
|
|
127
|
+
}
|
|
128
|
+
ask() {
|
|
129
|
+
rm -f "$work/message" "$work/header" "$work/errno"
|
|
130
|
+
{ { "$@"; } 2>&1 >&5 | relay; } 5>&1 &
|
|
131
|
+
wait "$!"
|
|
132
|
+
header=
|
|
133
|
+
errno=
|
|
134
|
+
message=
|
|
135
|
+
[ ! -f "$work/header" ] || header=$(cat "$work/header")
|
|
136
|
+
[ ! -f "$work/errno" ] || errno=$(cat "$work/errno")
|
|
137
|
+
[ ! -f "$work/message" ] || message=$(cat "$work/message")
|
|
138
|
+
errno=${errno:-$header}
|
|
139
|
+
[ -z "$errno" ]
|
|
140
|
+
}
|
|
141
|
+
because() {
|
|
142
|
+
printf '%s' "${message:-$errno}"
|
|
143
|
+
}
|
|
144
|
+
copyfail() {
|
|
145
|
+
case $header in
|
|
146
|
+
"Not supported") die "failed to copy to clipboard: this page does not copy images (no clipboard.write)" ;;
|
|
147
|
+
esac
|
|
148
|
+
die "failed to copy to clipboard: $(because)"
|
|
149
|
+
}
|
|
150
|
+
stop() {
|
|
151
|
+
[ ! -s "$work/pid" ] || kill -TERM "$(cat "$work/pid")" 2>/dev/null
|
|
152
|
+
wait
|
|
153
|
+
say interrupted
|
|
154
|
+
exit $((128 + $1))
|
|
155
|
+
}
|
|
156
|
+
absolute() {
|
|
157
|
+
case $1 in
|
|
158
|
+
*/*) dir=${1%/*} base=${1##*/} ;;
|
|
159
|
+
*) dir=. base=$1 ;;
|
|
160
|
+
esac
|
|
161
|
+
dir=$(cd -- "${dir:-/}" && pwd)
|
|
162
|
+
printf '%s/%s' "${dir%/}" "$base"
|
|
163
|
+
}
|
|
164
|
+
cleanup() {
|
|
165
|
+
rm -rf "$work"
|
|
166
|
+
}
|
|
167
|
+
argc=$#
|
|
168
|
+
clip=
|
|
169
|
+
view=
|
|
170
|
+
cursor=never
|
|
171
|
+
surface=
|
|
172
|
+
wait=
|
|
173
|
+
area=
|
|
174
|
+
type=
|
|
175
|
+
style=
|
|
176
|
+
off=
|
|
177
|
+
problem=
|
|
178
|
+
wrong=
|
|
179
|
+
special=
|
|
180
|
+
help=
|
|
181
|
+
ended=
|
|
182
|
+
files=0
|
|
183
|
+
file=
|
|
184
|
+
while [ $# -gt 0 ]; do
|
|
185
|
+
arg=$1
|
|
186
|
+
shift
|
|
187
|
+
if [ -n "$ended" ]; then
|
|
188
|
+
files=$((files + 1))
|
|
189
|
+
file=${file:-$arg}
|
|
190
|
+
continue
|
|
191
|
+
fi
|
|
192
|
+
case $arg in
|
|
193
|
+
--) ended=1 ;;
|
|
194
|
+
--help) help=1 ;;
|
|
195
|
+
--version | --upgrade) special=${special:-$arg} ;;
|
|
196
|
+
--clipboard) clip=1 ;;
|
|
197
|
+
--view) view=1 ;;
|
|
198
|
+
--*) wrong=${wrong:-"unknown option: $arg"} ;;
|
|
199
|
+
-?*)
|
|
200
|
+
rest=${arg#-}
|
|
201
|
+
while [ -n "$rest" ]; do
|
|
202
|
+
letter=${rest%"${rest#?}"}
|
|
203
|
+
rest=${rest#?}
|
|
204
|
+
case $letter in
|
|
205
|
+
h) help=1 ;;
|
|
206
|
+
[BDGJlRtTV])
|
|
207
|
+
if [ -n "$rest" ]; then
|
|
208
|
+
value "$letter" "$rest"
|
|
209
|
+
elif [ $# -gt 0 ]; then
|
|
210
|
+
value "$letter" "$1"
|
|
211
|
+
shift
|
|
212
|
+
else
|
|
213
|
+
wrong=${wrong:-"-$letter: option requires a value"}
|
|
214
|
+
fi
|
|
215
|
+
rest=
|
|
216
|
+
;;
|
|
217
|
+
[abcCdgikmMoprPsSuUvwWx]) flag "$letter" ;;
|
|
218
|
+
*)
|
|
219
|
+
wrong=${wrong:-"unknown option: -$letter"}
|
|
220
|
+
rest=
|
|
221
|
+
;;
|
|
222
|
+
esac
|
|
223
|
+
done
|
|
224
|
+
;;
|
|
225
|
+
*)
|
|
226
|
+
files=$((files + 1))
|
|
227
|
+
file=${file:-$arg}
|
|
228
|
+
;;
|
|
229
|
+
esac
|
|
230
|
+
done
|
|
231
|
+
if [ -n "$help" ]; then
|
|
232
|
+
usage
|
|
233
|
+
exit 0
|
|
234
|
+
fi
|
|
235
|
+
[ -z "$wrong" ] || bad "$wrong"
|
|
236
|
+
if [ -n "$special" ]; then
|
|
237
|
+
[ "$argc" -eq 1 ] || bad "$special: takes no other arguments"
|
|
238
|
+
if [ "$special" = --upgrade ]; then
|
|
239
|
+
printf 'pnpm add -g @ai-ecoverse/slicc-capture@latest\n'
|
|
240
|
+
else
|
|
241
|
+
version
|
|
242
|
+
fi
|
|
243
|
+
exit 0
|
|
244
|
+
fi
|
|
245
|
+
[ -z "$off" ] || refuse "$off: not supported in SLICC"
|
|
246
|
+
[ -z "$problem" ] || refuse "$problem"
|
|
247
|
+
[ -z "$style" ] || refuse "$style"
|
|
248
|
+
mime=png
|
|
249
|
+
if [ -n "$clip" ]; then
|
|
250
|
+
file=
|
|
251
|
+
files=0
|
|
252
|
+
elif [ -n "$type" ]; then
|
|
253
|
+
mime=$(format "$type")
|
|
254
|
+
[ -n "$mime" ] || refuse "-t $type: not supported in SLICC"
|
|
255
|
+
else
|
|
256
|
+
case $file in
|
|
257
|
+
*.[jJ][pP][gG] | *.[jJ][pP][eE][gG]) mime=jpeg ;;
|
|
258
|
+
*.[wW][eE][bB][pP]) mime=webp ;;
|
|
259
|
+
esac
|
|
260
|
+
fi
|
|
261
|
+
[ "$files" -le 1 ] || refuse "more than one file: not supported in SLICC"
|
|
262
|
+
[ -n "$file" ] || [ -n "$clip" ] || [ -n "$view" ] || bad "output file required (or use -c for clipboard)"
|
|
263
|
+
[ -c "$dev" ] || die "$dev not available (needs slicc-kernel >= 1.153.0)"
|
|
264
|
+
request="{\"kind\":\"capture.screen\",\"type\":\"image/$mime\""
|
|
265
|
+
[ "$mime" = png ] || request="$request,\"quality\":0.92"
|
|
266
|
+
request="$request,\"cursor\":\"$cursor\""
|
|
267
|
+
[ -z "$surface" ] || request="$request,\"surface\":\"$surface\""
|
|
268
|
+
[ -z "$wait" ] || request="$request,\"delay\":$(num "${wait%%.*}")${wait#"${wait%%.*}"}"
|
|
269
|
+
[ -z "$area" ] || request="$request,\"rect\":{\"x\":$(num "$rx"),\"y\":$(num "$ry"),\"width\":$(num "$rw"),\"height\":$(num "$rh")}"
|
|
270
|
+
request="$request}"
|
|
271
|
+
work=$(mktemp -d)
|
|
272
|
+
trap cleanup EXIT
|
|
273
|
+
trap 'stop 1' HUP
|
|
274
|
+
trap 'stop 2' INT
|
|
275
|
+
trap 'stop 15' TERM
|
|
276
|
+
if [ -n "$clip" ]; then
|
|
277
|
+
exec 4<>"$dev"
|
|
278
|
+
ask sh -c "printf '%s\\n' \"\$1\" >&4" sh '{"kind":"clipboard.write","type":"image/png"}' || copyfail
|
|
279
|
+
fi
|
|
280
|
+
exec 3<>"$dev"
|
|
281
|
+
if ! ask sh -c "printf '%s\\n' \"\$1\" >&3 || exit 1; cat <&3 & echo \$! >\"\$2\"; wait \$!" sh "$request" "$work/pid" >"$work/image"; then
|
|
282
|
+
case $header in
|
|
283
|
+
"Not supported") die "browser APIs are unavailable in this environment" ;;
|
|
284
|
+
esac
|
|
285
|
+
case $errno in
|
|
286
|
+
"Permission denied") die "user cancelled, permission denied, or the surface is protected" ;;
|
|
287
|
+
"Not supported") die "${message:-screen capture is not supported in this browser}" ;;
|
|
288
|
+
esac
|
|
289
|
+
die "$(because)"
|
|
290
|
+
fi
|
|
291
|
+
exec 3<&-
|
|
292
|
+
size=$(wc -c <"$work/image")
|
|
293
|
+
[ "$size" -gt 0 ] || die "the page sent no image"
|
|
294
|
+
kb=$(((size + 512) / 1024))
|
|
295
|
+
if [ -n "$clip" ]; then
|
|
296
|
+
ask cat "$work/image" >&4 || copyfail
|
|
297
|
+
ask sh -c 'cat <&4' >/dev/null || copyfail
|
|
298
|
+
exec 4<&-
|
|
299
|
+
printf 'captured %s KB to clipboard\n' "$kb"
|
|
300
|
+
fi
|
|
301
|
+
if [ -z "$file" ]; then
|
|
302
|
+
[ -n "$view" ] || exit 0
|
|
303
|
+
file=$(mktemp "${TMPDIR:-/tmp}/screencapture-XXXXXX.$mime")
|
|
304
|
+
fi
|
|
305
|
+
err=$(tee -- "$file" <"$work/image" 2>&1 >/dev/null) || die "failed to write file: ${err#tee: }"
|
|
306
|
+
if [ -n "$view" ]; then
|
|
307
|
+
printf '<img:%s>\n' "$(absolute "$file")"
|
|
308
|
+
elif [ -z "$clip" ]; then
|
|
309
|
+
printf 'captured %s KB to %s\n' "$kb" "$file"
|
|
310
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-capture",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "screencapture for SLICC, and sccapture for SLICC and any host",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"url": "git+https://github.com/ai-ecoverse/slicc-capture.git"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
|
-
"node": ">=24"
|
|
12
|
+
"node": ">=24",
|
|
13
|
+
"slicc-kernel": ">=1.153.0"
|
|
13
14
|
},
|
|
14
15
|
"bin": {
|
|
15
16
|
"sccapture": "host/sccapture.mjs"
|
|
@@ -25,30 +26,44 @@
|
|
|
25
26
|
"provenance": true
|
|
26
27
|
},
|
|
27
28
|
"slicc": {
|
|
29
|
+
"commands": {
|
|
30
|
+
"screencapture": {
|
|
31
|
+
"script": "bin/screencapture"
|
|
32
|
+
},
|
|
33
|
+
"sccapture": {
|
|
34
|
+
"script": "bin/sccapture"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
28
37
|
"unitSources": [
|
|
29
38
|
"host"
|
|
30
39
|
]
|
|
31
40
|
},
|
|
32
41
|
"files": [
|
|
42
|
+
"bin",
|
|
33
43
|
"host"
|
|
34
44
|
],
|
|
35
45
|
"scripts": {
|
|
36
46
|
"prepare": "slicc-install-hooks",
|
|
37
|
-
"lint": "slicc-lint && shellcheck -s sh install.sh && dash -n
|
|
47
|
+
"lint": "slicc-lint && shellcheck -s sh -e SC1091 install.sh bin/* && for f in install.sh bin/*; do dash -n \"$f\" || exit 1; done",
|
|
38
48
|
"format": "biome check --write .",
|
|
39
49
|
"test": "node --test --test-timeout=120000 test/integration/*.test.mjs",
|
|
40
50
|
"test:unit": "mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'",
|
|
51
|
+
"test:sh": "node scripts/cover.mjs",
|
|
41
52
|
"build": "npm pack --dry-run",
|
|
42
53
|
"trust": "npx fledgling sync -y --skip-publish",
|
|
43
54
|
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
|
|
44
55
|
"release": "semantic-release"
|
|
45
56
|
},
|
|
46
57
|
"devDependencies": {
|
|
58
|
+
"@ai-ecoverse/slicc-kernel": "1.153.0",
|
|
47
59
|
"@ai-ecoverse/slicc-shared-web": "2.11.3",
|
|
60
|
+
"@ai-ecoverse/wasm-bash": "5.3.0-23",
|
|
61
|
+
"@ai-ecoverse/wasm-coreutils": "9.12.0-15",
|
|
48
62
|
"@biomejs/biome": "2.5.15",
|
|
49
63
|
"@semantic-release/exec": "7.1.0",
|
|
50
64
|
"lefthook": "2.2.2",
|
|
51
65
|
"monocart-coverage-reports": "2.13.2",
|
|
52
|
-
"semantic-release": "25.0.9"
|
|
66
|
+
"semantic-release": "25.0.9",
|
|
67
|
+
"slicc-kernel-without-embedder": "npm:@ai-ecoverse/slicc-kernel@1.152.2"
|
|
53
68
|
}
|
|
54
69
|
}
|