@ai-ecoverse/slicc-capture 0.0.0-stage → 1.0.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/LICENSE +201 -0
- package/README.md +204 -2
- package/host/capture.mjs +61 -0
- package/host/darwin.mjs +26 -0
- package/host/linux.mjs +131 -0
- package/host/options.mjs +111 -0
- package/host/sccapture.mjs +4 -0
- package/host/tools.mjs +87 -0
- package/host/upgrade.mjs +110 -0
- package/host/windows.mjs +125 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,205 @@
|
|
|
1
|
-
#
|
|
1
|
+
# slicc-capture
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Screen capture for SLICC: `screencapture`, with the options of macOS's `screencapture(1)` wherever they make sense in a browser, and slicc 6's `-c` (to the clipboard) and `-v` (inline in the agent's tool result). Seven's page shows a prompt, the user picks a screen, window or tab in Chrome's `getDisplayMedia` picker, and the command writes a PNG (or JPEG or WebP).
|
|
4
|
+
|
|
5
|
+
`sccapture` is the same command under a name that works everywhere: in SLICC it is `screencapture`, and on macOS, Linux and Windows it hands off to the platform's own screenshot tool (see [On a host](#on-a-host)). A host's own `screencapture` is never shadowed.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
screencapture shot.png
|
|
9
|
+
screencapture -c
|
|
10
|
+
screencapture -v
|
|
11
|
+
screencapture -T 3 -R 0,0,800,600 -t jpg crop.jpg
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
In SLICC, the package declares `screencapture` and `sccapture` in `slicc.commands`, so installing it puts both in `/usr/bin`:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm add -g @ai-ecoverse/slicc-capture
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
On macOS and Linux:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
curl -fsSL https://get.sliccy.ai/slicc-capture | sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
On Windows, in PowerShell:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
irm https://get.sliccy.ai/slicc-capture.ps1 | iex
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or, on any machine with Node 24 or later:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install -g @ai-ecoverse/slicc-capture
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
or without installing, `npx -p @ai-ecoverse/slicc-capture sccapture shot.png`.
|
|
41
|
+
|
|
42
|
+
### The installers
|
|
43
|
+
|
|
44
|
+
Each release attaches `install.sh` and `install.ps1` next to the package tarball, `slicc-capture-<version>.tgz`, and a `SHA256SUMS` that lists all three. The PR that adds the installers also adds them, the tarball and `SHA256SUMS` to `@semantic-release/github`'s `assets` in `.releaserc.json`, with a test in `test/integration/repo.test.mjs`, so no release ships without them. get.sliccy.ai serves the latest release's installers. Both work like mcpcat's `install.sh`:
|
|
45
|
+
|
|
46
|
+
- **Options:** `--version <version>` installs that release instead of the latest; `--dir <dir>` puts the launcher there instead of the default; `--help` prints the usage and exits 0. An unknown option prints `slicc-capture: unknown option: <arg>` to stderr and exits 1. The environment variables `VERSION` and `INSTALL_DIR` do the same as the options. Through a pipe, `sh -s -- --dir ~/bin` passes options to `install.sh`, and `& ([scriptblock]::Create((irm https://get.sliccy.ai/slicc-capture.ps1))) --dir C:\tools` to `install.ps1`.
|
|
47
|
+
- **Node 24 or later:** the `node` on `PATH` if it is new enough, else the one the installer downloaded before; else it downloads the current Node LTS from nodejs.org, checks it against that release's `SHASUMS256.txt`, and keeps it in the data directory. On musl Linux, where nodejs.org has no build, it says to install Node with the package manager and exits 1.
|
|
48
|
+
- **Where it goes:** the tarball is checked against `SHA256SUMS`, unpacked into `<data>/<version>`, and `<data>/current` points at it. `<data>` is `${XDG_DATA_HOME:-~/.local/share}/slicc-capture` on macOS and Linux, `%LOCALAPPDATA%\slicc-capture` on Windows. The launcher, `sccapture` (`sccapture.cmd` on Windows), goes into `~/.local/bin` (`%LOCALAPPDATA%\slicc-capture\bin` on Windows) and runs that Node on `<data>/<version>/host/sccapture.mjs`, so the launcher keeps working when the `node` on `PATH` changes.
|
|
49
|
+
- **Output:** `sccapture <version> installed to <launcher>`, and, when the directory isn't on `PATH`, the line that adds it. Any failure (no `curl` or `wget`, a checksum mismatch, an unsupported OS or architecture) is one line on stderr and exit 1; on an unsupported OS it names `npm install -g @ai-ecoverse/slicc-capture` instead.
|
|
50
|
+
|
|
51
|
+
The get.sliccy.ai entries for `slicc-capture` and `slicc-capture.ps1` are added once the first release is out.
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| command | where | what |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `screencapture` | SLICC | captures through seven's page, as below |
|
|
58
|
+
| `sccapture` | SLICC | the same as `screencapture` |
|
|
59
|
+
| `sccapture` | macOS, Linux, Windows | hands off to the platform's tool, as in [On a host](#on-a-host) |
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
usage: screencapture [-acdimorvwxCW] [-t format] [-T seconds] [-R x,y,w,h] [-D display] [-J style] [file]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Short options may be grouped (`-xc`), an option's value may be attached or separate (`-T3`, `-T 3`), and `--` ends the options. `--clipboard` is `-c` and `--view` is `-v`, as in slicc 6. `-h` or `--help` anywhere prints the usage to stdout and exits 0. `--version` and `--upgrade` are described [below](#--upgrade); either must be the only argument, else `<cmd>: <opt>: takes no other arguments` and the usage to stderr, exit 2. An option without its value prints `<cmd>: <opt>: option requires a value` and the usage to stderr, exit 2.
|
|
66
|
+
|
|
67
|
+
### In SLICC
|
|
68
|
+
|
|
69
|
+
`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 command waits for `-T` seconds, if given, takes one frame, crops it to `-R`, if given, encodes it and stops the capture.
|
|
70
|
+
|
|
71
|
+
- **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>`.
|
|
72
|
+
- **`-c`:** copies the capture to the clipboard as `image/png`, through [slicc-clipboard](https://github.com/ai-ecoverse/slicc-clipboard), and prints `captured <n> KB to clipboard`. A file named with `-c` is ignored, as with macOS's `-c`, and so is `-t`. slicc-clipboard's device is text only today; `-c` needs its image path, which is being added there.
|
|
73
|
+
- **`-v`:** shows the capture inline in the agent's tool result. With a file, it writes the file and prints `<path> (<n> KB)` followed by the image; without one, it prints only the image; with `-c`, it copies and shows. slicc 6 printed the image as `<img:data:<type>;base64,<data>>`; seven's marker for an inline image is being agreed with slicc-agent, and `-v` uses whatever that is. macOS's `-v` (record a video) is out of scope everywhere.
|
|
74
|
+
- **No file:** without a file, `-c` or `-v`: `screencapture: output file required (or use -c for clipboard)` and the usage to stderr, exit 2.
|
|
75
|
+
|
|
76
|
+
Errors, on stderr, from slicc 6 where they still apply:
|
|
77
|
+
|
|
78
|
+
| error | exit |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `screencapture: user cancelled or permission denied` (**Cancel**, the picker closed, or the capture refused) | 1 |
|
|
81
|
+
| `screencapture: timed out waiting for the picker` (no answer within 5 minutes) | 1 |
|
|
82
|
+
| `screencapture: browser APIs are unavailable in this environment` (an embedder that doesn't answer capture requests) | 1 |
|
|
83
|
+
| `screencapture: screen capture is not supported in this browser` (no `getDisplayMedia`) | 1 |
|
|
84
|
+
| `screencapture: -R: rectangle outside the capture` | 1 |
|
|
85
|
+
| `screencapture: failed to write file: <message>` | 1 |
|
|
86
|
+
| `screencapture: failed to copy to clipboard: <message>` | 1 |
|
|
87
|
+
| `screencapture: unknown option: <arg>`, then the usage | 2 |
|
|
88
|
+
| `screencapture: <opt>: not supported in SLICC` | 2 |
|
|
89
|
+
| `screencapture: -t <format>: not supported in SLICC` | 2 |
|
|
90
|
+
| `screencapture: -T: invalid delay: <value>`, `-R: invalid rectangle: <value>`, `-D: invalid display: <value>`, `-J: invalid style: <value>` | 2 |
|
|
91
|
+
| `screencapture: more than one file: not supported in SLICC` | 2 |
|
|
92
|
+
|
|
93
|
+
As `sccapture`, the messages start with `sccapture:`.
|
|
94
|
+
|
|
95
|
+
**The bridge from the kernel to the page is not settled yet.** The commands run in a kernel worker, which has no `getDisplayMedia`, so a request has to reach seven's page and the frame has to come back. slicc-clipboard does this with a kernel character device that the kernel answers from its embedder's hook; slicc-open, slicc-capture and slicc-audio are agreeing one pattern for all three in [ai-ecoverse/slicc-kernel#1527](https://github.com/ai-ecoverse/slicc-kernel/issues/1527) (pending), with the kernel and seven's embedder (slicc-bios). Until then this section describes only what the user sees. The page side (the prompt, `getDisplayMedia`, the frame, the crop and the encoding) ships in this package for the embedder; its entry point, the transport and the `engines.slicc-kernel` it needs are added here once the pattern is agreed.
|
|
96
|
+
|
|
97
|
+
### Options
|
|
98
|
+
|
|
99
|
+
Every option of macOS's `screencapture`, and what it does in SLICC and with `sccapture` on each host. On macOS, `sccapture` passes the options through to `screencapture` unchanged, except the ones the macOS column names. "No-op" means the option is accepted and changes nothing. "Not supported" means `<cmd>: <opt>: not supported in SLICC` (or `on Linux`, `on Windows`, `on macOS`) on stderr, exit 2, before anything is captured; "not with `<tool>`" means `sccapture: <opt>: not supported with <tool>`, exit 2, when no tool on `PATH` supports every option given (it names the first one tried).
|
|
100
|
+
|
|
101
|
+
| option | macOS `screencapture` | SLICC | macOS `sccapture` | Linux `sccapture` | Windows `sccapture` |
|
|
102
|
+
| --- | --- | --- | --- | --- | --- |
|
|
103
|
+
| `-a` | don't capture attached windows | no-op | passed | no-op | no-op |
|
|
104
|
+
| `-b` | capture the Touch Bar | not supported | passed | not supported | not supported |
|
|
105
|
+
| `-B <bundleid>` | open in that app | not supported | passed | not supported | not supported |
|
|
106
|
+
| `-c` | to the clipboard | through slicc-clipboard, as `image/png` | passed | see [Linux](#linux) | `Clipboard.SetImage` |
|
|
107
|
+
| `-C` | include the cursor | asks for `cursor: 'always'` (else `'never'`), which the browser may not honour | passed | see [Linux](#linux) | draws the cursor into the image |
|
|
108
|
+
| `-d` | show errors graphically | no-op | passed | no-op | no-op |
|
|
109
|
+
| `-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 |
|
|
110
|
+
| `-g` | record default audio input | not supported | not supported | not supported | not supported |
|
|
111
|
+
| `-G <id>` | record that audio source | not supported | not supported | not supported | not supported |
|
|
112
|
+
| `-i` | interactive, by selection or window | no-op: the picker is the interaction | passed | see [Linux](#linux) | not supported |
|
|
113
|
+
| `-J <style>` | start in `selection`, `window` or `video` | `window` as `-W`; `selection` and `video` not supported | passed | `selection` as `-s`, `window` as `-w`, `video` not supported | not supported |
|
|
114
|
+
| `-k` | show clicks in a recording | not supported | not supported | not supported | not supported |
|
|
115
|
+
| `-l <windowid>` | that window | not supported | passed | see [Linux](#linux) | not supported |
|
|
116
|
+
| `-m` | the main monitor only | prefers `displaySurface: 'monitor'`; the user picks the screen | passed | not supported | the primary screen |
|
|
117
|
+
| `-M` | open in a new Mail message | not supported | passed | not supported | not supported |
|
|
118
|
+
| `-o` | no window shadow | no-op: the browser captures none | passed | no-op | no-op |
|
|
119
|
+
| `-p` | default settings, file ignored | not supported | passed | not supported | not supported |
|
|
120
|
+
| `-P` | open in Preview | not supported | passed | `xdg-open <file>` afterwards; without it, `sccapture: no viewer found (tried xdg-open)`, exit 1, after the file is written | `Invoke-Item <file>` afterwards |
|
|
121
|
+
| `-r` | no dpi metadata | no-op: none is written | passed | no-op | no-op |
|
|
122
|
+
| `-R <x,y,w,h>` | that rectangle | crops the frame, in its pixels | passed | see [Linux](#linux) | that rectangle of the virtual screen |
|
|
123
|
+
| `-s` | mouse selection only | not supported: use `-R` | passed | see [Linux](#linux) | not supported |
|
|
124
|
+
| `-S` | the screen, in window mode | not supported | passed | not supported | not supported |
|
|
125
|
+
| `-t <format>` | the image format, `png` by default | `png`, `jpg` or `jpeg`, `webp`; others not supported | passed | see [Linux](#linux) | `png`, `jpg` or `jpeg`, `bmp`, `gif`, `tiff`; others not supported |
|
|
126
|
+
| `-T <seconds>` | wait before capturing | waits after the picker | passed | `sccapture` waits, then runs the tool | `sccapture` waits, then captures |
|
|
127
|
+
| `-u` | show the UI afterwards, file ignored | not supported | passed | not supported | not supported |
|
|
128
|
+
| `-U` | the interactive toolbar | not supported | passed | not supported | not supported |
|
|
129
|
+
| `-v` | record a video | inline in the tool result (slicc 6's `-v`) | not supported | not supported | not supported |
|
|
130
|
+
| `-V <seconds>` | record a video for that long | not supported | not supported | not supported | not supported |
|
|
131
|
+
| `-w` | window selection only | prefers `displaySurface: 'window'` | passed | see [Linux](#linux) | not supported |
|
|
132
|
+
| `-W` | start in window selection | prefers `displaySurface: 'window'` | passed | as `-w` | not supported |
|
|
133
|
+
| `-x` | no sounds | no-op | passed | no-op | no-op |
|
|
134
|
+
| `file ...` | one file per screen | one file; more not supported | passed | one file; more not supported | one file; more not supported |
|
|
135
|
+
|
|
136
|
+
Video recording (`-v`, `-V`, `-g`, `-G`, `-k`) is out of scope on every platform, macOS included; a later release may add it, sharing an encoder with whatever replaces `ffmpeg` in SLICC. `--clipboard` and `--view` are `-c` and `-v` on every platform; on macOS, `sccapture` passes `--clipboard` on as `-c`.
|
|
137
|
+
|
|
138
|
+
## On a host
|
|
139
|
+
|
|
140
|
+
Only `sccapture` is installed on a host (the package's `bin`, a small Node script, `host/sccapture.mjs`); the host's own `screencapture`, `grim` and the rest are never shadowed. It runs the platform's tool with a fixed argv and no shell: of the tools below, in this order, the first one on `PATH` that supports every option given (so on GNOME under X11, `-l` picks `import` over `gnome-screenshot`):
|
|
141
|
+
|
|
142
|
+
| platform | tool | `-c` |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| macOS | `screencapture`, with the options passed through as above | `screencapture -c` |
|
|
145
|
+
| Linux, under Wayland (`WAYLAND_DISPLAY` set) | `grim` (with `slurp` to select), else `gnome-screenshot` | `wl-copy --type image/png`, else `xclip -selection clipboard -t image/png` |
|
|
146
|
+
| Linux, X11 | `gnome-screenshot`, else ImageMagick `import`, else `scrot` | `xclip -selection clipboard -t image/png` |
|
|
147
|
+
| Windows | PowerShell, `powershell.exe` else `pwsh.exe`, with System.Drawing and System.Windows.Forms | `Clipboard.SetImage` |
|
|
148
|
+
|
|
149
|
+
- **The file and its format:** without `-t`, the file's extension picks the format (on macOS, `sccapture` adds `-t` for `.jpg`, `.jpeg`, `.pdf`, `.tiff`, `.gif` and `.bmp`), else PNG. `-c` copies PNG; a file, `-t` and `-P` are then ignored, except on macOS, where `screencapture` decides. Success prints nothing, as `screencapture` does.
|
|
150
|
+
- **A file is required**, as in SLICC (`sccapture: output file required (or use -c for clipboard)`, exit 2), except on macOS with `-p`, `-u`, `-P`, `-M` or `-B`, which `screencapture` handles without one.
|
|
151
|
+
- **Errors:** with no tool on `PATH`, `sccapture: no screenshot tool found (tried grim, gnome-screenshot)` (the names tried on that platform) to stderr, exit 1; with `-c` and no clipboard tool, `sccapture: no clipboard tool found (tried wl-copy, xclip)`, exit 1. A tool that fails, a cancelled selection included, has its stderr passed through, exit 1. An unknown option prints `sccapture: unknown option: <arg>` and the usage to stderr, exit 2; an option the table marks as not supported on that platform prints `sccapture: <opt>: not supported on <platform>`, exit 2. The values of `-T`, `-R`, `-D` and `-J` are checked as in SLICC, and `-l` takes a window id in decimal or `0x` hex, else `sccapture: -l: invalid window: <value>`, exit 2. `-h` and `--help` print the usage to stdout and exit 0.
|
|
152
|
+
- **`--upgrade`:** the command counts as installed by the installer when it runs from `<data>/<version>/host/sccapture.mjs` with a `<data>/current` next to it, and as installed with npm otherwise. `--dir` is the directory on `PATH` whose `sccapture` (`sccapture.cmd` on Windows) names `<data>`, else the installer's default. It downloads the installer with `curl`, else `wget` (`curl.exe` on Windows), and without one prints `sccapture: no downloader found (tried curl, wget)`, exit 1; a failed download has its stderr passed through, exit 1. The installer runs with `sh` (`powershell.exe`, else `pwsh.exe`, with `-ExecutionPolicy Bypass -File` on Windows), without `VERSION` and `INSTALL_DIR` in its environment, so it installs the latest release where the command is; the new version is read from `<data>/current/package.json`.
|
|
153
|
+
- **Other Unix systems** are treated as Linux.
|
|
154
|
+
|
|
155
|
+
### Linux
|
|
156
|
+
|
|
157
|
+
How each option reaches the tool. "Not with `<tool>`" is the error only when no tool on `PATH` supports every option given, and it names the first one tried. `<file>` is the file, or a temporary PNG that is then copied with `-c` and deleted.
|
|
158
|
+
|
|
159
|
+
| option | `grim` | `gnome-screenshot` | `import` | `scrot` |
|
|
160
|
+
| --- | --- | --- | --- | --- |
|
|
161
|
+
| none | `grim <file>` | `gnome-screenshot -f <file>` | `import -window root <file>` | `scrot -o <file>` |
|
|
162
|
+
| `-i`, `-s` | `grim -g <slurp's output> <file>`; without `slurp`, `sccapture: no region selector found (tried slurp)`, exit 1 | `-a` | `import <file>`: click a window or drag a region | `-s` |
|
|
163
|
+
| `-w`, `-W` | not with `grim` | not with `gnome-screenshot` | `import <file>` | `-s` |
|
|
164
|
+
| `-R x,y,w,h` | `-g "x,y wxh"` | not with `gnome-screenshot` | `-crop wxh+x+y +repage` | `-a x,y,w,h` |
|
|
165
|
+
| `-C` | `-c` | `-p` | not with `import` | `-p` |
|
|
166
|
+
| `-l <windowid>` | not with `grim` | not with `gnome-screenshot` | `-window <windowid>` | not with `scrot` |
|
|
167
|
+
| `-t png`, `jpg` | `-t png`, `-t jpeg`; without `-t`, `-t jpeg` for a `.jpg` or `.jpeg` file, since grim writes PNG whatever the extension | `png` only, else not with `gnome-screenshot` | `png:<file>`, `jpg:<file>`; without `-t`, `png:<file>` for a file without an extension, which `import` can't guess | by the file's extension; a `-t` that differs from it is not with `scrot` |
|
|
168
|
+
|
|
169
|
+
Under Wayland, `grim` needs a compositor with wlr-screencopy (sway, Hyprland and other wlroots ones); elsewhere it fails with its own message, so install `gnome-screenshot` there. `import` and `scrot` aren't tried under Wayland, where they would see only XWayland windows.
|
|
170
|
+
|
|
171
|
+
### Windows
|
|
172
|
+
|
|
173
|
+
`sccapture` runs `powershell.exe` (else `pwsh.exe`) with `-NoProfile -NonInteractive -STA -EncodedCommand` and a fixed script; the file name and the option values reach it as data, in `SCCAPTURE_*` environment variables, never as script. (PowerShell reads a script on stdin line by line, as if typed, so a multi-line block in it can stop at the first blank line; `-EncodedCommand` is what slicc-clipboard uses.) The process is made per-monitor DPI-aware first (system DPI-aware before Windows 10 1703), so a scaled display is captured at its real resolution, even when the monitors' scales differ. It copies the virtual screen (every monitor), or the screen `-m`/`-D` names, or the `-R` rectangle of that (its `x,y` from the top left of the virtual screen, or of the screen `-m`/`-D` names), with `Graphics.CopyFromScreen`, draws the cursor for `-C`, and saves with the `-t` format's `ImageFormat`, or `Clipboard.SetImage` for `-c`.
|
|
174
|
+
|
|
175
|
+
### In SLICC, the SLICC commands win
|
|
176
|
+
|
|
177
|
+
The package has both `bin` and `slicc.commands`, and both name `sccapture`. As with slicc-clipboard, the kernel ignores a package's `bin` entry when the same package declares that name in `slicc.commands`, and bios removes the `$PNPM_HOME/bin` link pnpm makes for it, so the Node script, which SLICC has no Node to run, never shadows the SLICC command.
|
|
178
|
+
|
|
179
|
+
## `--upgrade`
|
|
180
|
+
|
|
181
|
+
Every host command takes `--upgrade`. It runs the latest release's installer (`install.sh`, or `install.ps1` on Windows) with `--dir` set to the directory the command is installed in, so the new version replaces the old one in place, then prints `<cmd>: <old> → <new>` (for example `sccapture: 1.2.0 → 1.3.0`), or `<cmd>: already up to date (<version>)`, to stdout and exits 0. If the installer fails, its stderr is passed through and the command exits 1. A command installed with npm prints `npm install -g @ai-ecoverse/slicc-capture@latest` to run instead, and exits 0. In SLICC, `--upgrade` prints `pnpm add -g @ai-ecoverse/slicc-capture@latest` to run, and exits 0; it installs nothing itself. `--upgrade` takes no other arguments.
|
|
182
|
+
|
|
183
|
+
For slicc-capture, the commands are `sccapture` on a host and `screencapture` and `sccapture` in SLICC.
|
|
184
|
+
|
|
185
|
+
`--version` prints the version (`0.1.0`) to stdout and exits 0, on a host and in SLICC; the installers call the launcher with it. It takes no other arguments either.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
npm install
|
|
191
|
+
npm run lint
|
|
192
|
+
npm test
|
|
193
|
+
npm run test:unit
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- `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/`.
|
|
197
|
+
- `npm run lint`: Biome, the no-comment check and the other [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web) checks.
|
|
198
|
+
- `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`, `pwsh.exe`, `curl`, `wget` and a fake installer on a temporary `PATH`, with the platform forced, so each branch is checked on any machine; only CI touches a real screen.
|
|
199
|
+
- `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.
|
|
200
|
+
|
|
201
|
+
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.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
Apache-2.0
|
package/host/capture.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { setTimeout } from 'node:timers/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { darwin } from './darwin.mjs';
|
|
6
|
+
import { linux } from './linux.mjs';
|
|
7
|
+
import { check, names, parse, settings, usage } from './options.mjs';
|
|
8
|
+
import { say } from './tools.mjs';
|
|
9
|
+
import { upgrade } from './upgrade.mjs';
|
|
10
|
+
import { windows } from './windows.mjs';
|
|
11
|
+
|
|
12
|
+
const optional = 'puPMB';
|
|
13
|
+
|
|
14
|
+
export async function run({
|
|
15
|
+
argv = process.argv.slice(2),
|
|
16
|
+
env = process.env,
|
|
17
|
+
platform = process.platform,
|
|
18
|
+
fds = [0, 1, 2],
|
|
19
|
+
entry = fileURLToPath(new URL('sccapture.mjs', import.meta.url)),
|
|
20
|
+
wait = (seconds) => setTimeout(seconds * 1000),
|
|
21
|
+
} = {}) {
|
|
22
|
+
const os = names[platform] ? platform : 'linux';
|
|
23
|
+
const parsed = parse(argv);
|
|
24
|
+
if (parsed.help) {
|
|
25
|
+
say(fds[1], usage);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
if (parsed.error) {
|
|
29
|
+
say(fds[2], `sccapture: ${parsed.error}\n${usage}`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
if (parsed.special.length) {
|
|
33
|
+
if (argv.length > 1) {
|
|
34
|
+
say(fds[2], `sccapture: ${parsed.special[0]}: takes no other arguments\n${usage}`);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
if (parsed.special[0] === '--upgrade') return upgrade({ entry, env, platform: os, fds });
|
|
38
|
+
const pkg = JSON.parse(readFileSync(join(dirname(dirname(entry)), 'package.json'), 'utf8'));
|
|
39
|
+
say(fds[1], `${pkg.version}\n`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
const problem = check(parsed.options, os);
|
|
43
|
+
if (problem) {
|
|
44
|
+
say(fds[2], `sccapture: ${problem}\n`);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
const o = settings(parsed.options);
|
|
48
|
+
const free = os === 'darwin' && parsed.options.some((option) => optional.includes(option.letter));
|
|
49
|
+
if (!parsed.files.length && !o.clipboard && !free) {
|
|
50
|
+
say(fds[2], `sccapture: output file required (or use -c for clipboard)\n${usage}`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
if (os === 'darwin') return darwin({ parsed, o, env, platform: os, fds });
|
|
54
|
+
if (parsed.files.length > 1 && !o.clipboard) {
|
|
55
|
+
say(fds[2], `sccapture: more than one file: not supported on ${names[os]}\n`);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
const file = parsed.files.length ? resolve(parsed.files[0]) : undefined;
|
|
59
|
+
const context = { o, file, env, platform: os, fds, wait };
|
|
60
|
+
return os === 'win32' ? windows(context) : linux(context);
|
|
61
|
+
}
|
package/host/darwin.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { lookup, say, step } from './tools.mjs';
|
|
3
|
+
|
|
4
|
+
const extensions = {
|
|
5
|
+
'.jpg': 'jpg',
|
|
6
|
+
'.jpeg': 'jpg',
|
|
7
|
+
'.pdf': 'pdf',
|
|
8
|
+
'.tiff': 'tiff',
|
|
9
|
+
'.gif': 'gif',
|
|
10
|
+
'.bmp': 'bmp',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function darwin({ parsed, o, env, platform, fds }) {
|
|
14
|
+
const path = lookup('screencapture', platform, env);
|
|
15
|
+
if (!path) {
|
|
16
|
+
say(fds[2], 'sccapture: no screenshot tool found (tried screencapture)\n');
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const args = [...parsed.tokens];
|
|
20
|
+
const type =
|
|
21
|
+
!o.last.t && parsed.files.length && extensions[extname(parsed.files[0]).toLowerCase()];
|
|
22
|
+
if (type) args.push('-t', type);
|
|
23
|
+
if (parsed.files.some((file) => file.startsWith('-'))) args.push('--');
|
|
24
|
+
args.push(...parsed.files);
|
|
25
|
+
return (await step('screencapture', path, args, { env }, fds)) ? 0 : 1;
|
|
26
|
+
}
|
package/host/linux.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { lookup, say, step } from './tools.mjs';
|
|
5
|
+
|
|
6
|
+
const extensions = { '.png': 'png', '.jpg': 'jpg', '.jpeg': 'jpg' };
|
|
7
|
+
|
|
8
|
+
const builders = {
|
|
9
|
+
grim(o, file) {
|
|
10
|
+
if (o.window) return { unsupported: o.windowLabel };
|
|
11
|
+
if (o.id) return { unsupported: '-l' };
|
|
12
|
+
const args = o.cursor ? ['-c'] : [];
|
|
13
|
+
const type = o.type ?? extensions[extname(file).toLowerCase()];
|
|
14
|
+
if (o.type === 'png') args.push('-t', 'png');
|
|
15
|
+
else if (type === 'jpg') args.push('-t', 'jpeg');
|
|
16
|
+
if (o.rect) {
|
|
17
|
+
const [x, y, w, h] = o.rect;
|
|
18
|
+
args.push('-g', `${x},${y} ${w}x${h}`);
|
|
19
|
+
}
|
|
20
|
+
return { args: [...args, file], slurp: o.select && !o.rect };
|
|
21
|
+
},
|
|
22
|
+
'gnome-screenshot'(o, file) {
|
|
23
|
+
if (o.window) return { unsupported: o.windowLabel };
|
|
24
|
+
if (o.rect) return { unsupported: '-R' };
|
|
25
|
+
if (o.id) return { unsupported: '-l' };
|
|
26
|
+
if (o.type && o.type !== 'png') return { unsupported: `-t ${o.last.t.value}` };
|
|
27
|
+
const args = [];
|
|
28
|
+
if (o.select) args.push('-a');
|
|
29
|
+
if (o.cursor) args.push('-p');
|
|
30
|
+
return { args: [...args, '-f', file] };
|
|
31
|
+
},
|
|
32
|
+
import(o, file) {
|
|
33
|
+
if (o.cursor) return { unsupported: '-C' };
|
|
34
|
+
const args = [];
|
|
35
|
+
if (o.id) args.push('-window', o.id);
|
|
36
|
+
else if (!o.select && !o.window) args.push('-window', 'root');
|
|
37
|
+
if (o.rect) {
|
|
38
|
+
const [x, y, w, h] = o.rect;
|
|
39
|
+
args.push('-crop', `${w}x${h}+${x}+${y}`, '+repage');
|
|
40
|
+
}
|
|
41
|
+
const type = o.type ?? (extname(file) ? undefined : 'png');
|
|
42
|
+
return { args: [...args, type ? `${type}:${file}` : file] };
|
|
43
|
+
},
|
|
44
|
+
scrot(o, file) {
|
|
45
|
+
if (o.id) return { unsupported: '-l' };
|
|
46
|
+
if (o.type && extensions[extname(file).toLowerCase()] !== o.type) {
|
|
47
|
+
return { unsupported: `-t ${o.last.t.value}` };
|
|
48
|
+
}
|
|
49
|
+
const args = [];
|
|
50
|
+
if (o.select || o.window) args.push('-s');
|
|
51
|
+
if (o.rect) args.push('-a', o.rect.join(','));
|
|
52
|
+
if (o.cursor) args.push('-p');
|
|
53
|
+
return { args: [...args, '-o', file] };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function pick(names, o, file, platform, env) {
|
|
58
|
+
let first;
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
const path = lookup(name, platform, env);
|
|
61
|
+
if (!path) continue;
|
|
62
|
+
const built = builders[name](o, file);
|
|
63
|
+
if (!built.unsupported) return { name, path, ...built };
|
|
64
|
+
first ??= `${built.unsupported}: not supported with ${name}`;
|
|
65
|
+
}
|
|
66
|
+
return { error: first };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clipboard(wayland, platform, env) {
|
|
70
|
+
const xclip = { name: 'xclip', args: ['-selection', 'clipboard', '-t', 'image/png'] };
|
|
71
|
+
const clips = wayland ? [{ name: 'wl-copy', args: ['--type', 'image/png'] }, xclip] : [xclip];
|
|
72
|
+
for (const clip of clips) {
|
|
73
|
+
const path = lookup(clip.name, platform, env);
|
|
74
|
+
if (path) return { ...clip, path };
|
|
75
|
+
}
|
|
76
|
+
return { error: `no clipboard tool found (tried ${clips.map((c) => c.name).join(', ')})` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function linux({ o, file, env, platform, fds, wait }) {
|
|
80
|
+
const wayland = Boolean(env.WAYLAND_DISPLAY);
|
|
81
|
+
const names = wayland ? ['grim', 'gnome-screenshot'] : ['gnome-screenshot', 'import', 'scrot'];
|
|
82
|
+
const scratch = o.clipboard ? mkdtempSync(join(tmpdir(), 'sccapture-')) : undefined;
|
|
83
|
+
const target = scratch ? join(scratch, 'capture.png') : file;
|
|
84
|
+
try {
|
|
85
|
+
const tool = pick(names, o, target, platform, env);
|
|
86
|
+
if (!tool.name) {
|
|
87
|
+
if (tool.error) {
|
|
88
|
+
say(fds[2], `sccapture: ${tool.error}\n`);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
say(fds[2], `sccapture: no screenshot tool found (tried ${names.join(', ')})\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const clip = o.clipboard ? clipboard(wayland, platform, env) : undefined;
|
|
95
|
+
if (clip?.error) {
|
|
96
|
+
say(fds[2], `sccapture: ${clip.error}\n`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const slurp = tool.slurp ? lookup('slurp', platform, env) : undefined;
|
|
100
|
+
if (tool.slurp && !slurp) {
|
|
101
|
+
say(fds[2], 'sccapture: no region selector found (tried slurp)\n');
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
await wait(o.delay);
|
|
105
|
+
let args = tool.args;
|
|
106
|
+
if (slurp) {
|
|
107
|
+
const region = await step('slurp', slurp, [], { env, capture: true }, fds);
|
|
108
|
+
if (!region) return 1;
|
|
109
|
+
args = ['-g', region.stdout.trim(), ...args];
|
|
110
|
+
}
|
|
111
|
+
if (!(await step(tool.name, tool.path, args, { env }, fds))) return 1;
|
|
112
|
+
if (clip) {
|
|
113
|
+
const fd = openSync(target, 'r');
|
|
114
|
+
try {
|
|
115
|
+
if (!(await step(clip.name, clip.path, clip.args, { env, stdin: fd }, fds))) return 1;
|
|
116
|
+
} finally {
|
|
117
|
+
closeSync(fd);
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
if (!o.open) return 0;
|
|
122
|
+
const viewer = lookup('xdg-open', platform, env);
|
|
123
|
+
if (!viewer) {
|
|
124
|
+
say(fds[2], 'sccapture: no viewer found (tried xdg-open)\n');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
return (await step('xdg-open', viewer, [file], { env }, fds)) ? 0 : 1;
|
|
128
|
+
} finally {
|
|
129
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
package/host/options.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const flags = 'abcCdgiklmMoprPsSuUvwWx';
|
|
2
|
+
const valued = 'BDGJlRtTV';
|
|
3
|
+
|
|
4
|
+
export const usage =
|
|
5
|
+
'usage: sccapture [-acdimorvwxCW] [-t format] [-T seconds] [-R x,y,w,h] [-D display] [-J style] [file]\n';
|
|
6
|
+
|
|
7
|
+
const long = { '--clipboard': 'c', '--view': 'v' };
|
|
8
|
+
|
|
9
|
+
function group(argv, i, result, fail) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
let at = i;
|
|
12
|
+
for (let j = 1; j < arg.length; j++) {
|
|
13
|
+
const letter = arg[j];
|
|
14
|
+
if (letter === 'h') result.help = true;
|
|
15
|
+
else if (valued.includes(letter)) {
|
|
16
|
+
const value = j + 1 < arg.length ? arg.slice(j + 1) : argv[++at];
|
|
17
|
+
if (value === undefined) fail(`-${letter}: option requires a value`);
|
|
18
|
+
else result.options.push({ letter, label: `-${letter}`, value });
|
|
19
|
+
break;
|
|
20
|
+
} else if (flags.includes(letter)) result.options.push({ letter, label: `-${letter}` });
|
|
21
|
+
else fail(`unknown option: -${letter}`);
|
|
22
|
+
}
|
|
23
|
+
result.tokens.push(...argv.slice(i, at + 1));
|
|
24
|
+
return at;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parse(argv) {
|
|
28
|
+
const result = { help: false, error: undefined, special: [], options: [], files: [], tokens: [] };
|
|
29
|
+
const fail = (message) => {
|
|
30
|
+
result.error ??= message;
|
|
31
|
+
};
|
|
32
|
+
let ended = false;
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const arg = argv[i];
|
|
35
|
+
if (ended) result.files.push(arg);
|
|
36
|
+
else if (arg === '--') ended = true;
|
|
37
|
+
else if (arg === '--help') result.help = true;
|
|
38
|
+
else if (arg === '--version' || arg === '--upgrade') result.special.push(arg);
|
|
39
|
+
else if (long[arg]) {
|
|
40
|
+
result.options.push({ letter: long[arg], label: arg });
|
|
41
|
+
result.tokens.push(`-${long[arg]}`);
|
|
42
|
+
} else if (arg.startsWith('--')) fail(`unknown option: ${arg}`);
|
|
43
|
+
else if (arg.startsWith('-') && arg.length > 1) i = group(argv, i, result, fail);
|
|
44
|
+
else result.files.push(arg);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const unsupported = {
|
|
50
|
+
darwin: 'gGkvV',
|
|
51
|
+
linux: 'bBDgGkmMpSuUvV',
|
|
52
|
+
win32: 'bBgGiJklMpsSuUvVwW',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const formats = {
|
|
56
|
+
linux: ['png', 'jpg', 'jpeg'],
|
|
57
|
+
win32: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff'],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const checks = {
|
|
61
|
+
T: [/^\d+(\.\d+)?$/, 'invalid delay'],
|
|
62
|
+
R: [/^\d+,\d+,[1-9]\d*,[1-9]\d*$/, 'invalid rectangle'],
|
|
63
|
+
D: [/^[1-9]\d*$/, 'invalid display'],
|
|
64
|
+
J: [/^(selection|window|video)$/, 'invalid style'],
|
|
65
|
+
l: [/^(\d+|0x[0-9a-fA-F]+)$/, 'invalid window'],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const names = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
|
|
69
|
+
|
|
70
|
+
export function check(options, platform) {
|
|
71
|
+
const where = `on ${names[platform]}`;
|
|
72
|
+
const own = options.find((option) => unsupported[platform].includes(option.letter));
|
|
73
|
+
if (own) return `${own.label}: not supported ${where}`;
|
|
74
|
+
for (const option of options) {
|
|
75
|
+
const rule = checks[option.letter];
|
|
76
|
+
if (rule && !rule[0].test(option.value))
|
|
77
|
+
return `-${option.letter}: ${rule[1]}: ${option.value}`;
|
|
78
|
+
}
|
|
79
|
+
if (platform === 'darwin') return undefined;
|
|
80
|
+
const style = options.find((option) => option.letter === 'J' && option.value === 'video');
|
|
81
|
+
if (style) return `-J video: not supported ${where}`;
|
|
82
|
+
if (options.some((option) => option.letter === 'c')) return undefined;
|
|
83
|
+
const type = options.find(
|
|
84
|
+
(option) => option.letter === 't' && !formats[platform].includes(option.value.toLowerCase())
|
|
85
|
+
);
|
|
86
|
+
if (type) return `-t ${type.value}: not supported ${where}`;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function settings(options) {
|
|
91
|
+
const last = {};
|
|
92
|
+
for (const option of options) last[option.letter] = option;
|
|
93
|
+
const style = last.J?.value;
|
|
94
|
+
const label = (...letters) => letters.map((letter) => last[letter]?.label).find(Boolean);
|
|
95
|
+
const rect = last.R?.value.split(',').map(Number);
|
|
96
|
+
return {
|
|
97
|
+
last,
|
|
98
|
+
clipboard: Boolean(last.c),
|
|
99
|
+
cursor: Boolean(last.C),
|
|
100
|
+
open: Boolean(last.P),
|
|
101
|
+
main: Boolean(last.m),
|
|
102
|
+
display: last.D?.value,
|
|
103
|
+
delay: last.T ? Number(last.T.value) : 0,
|
|
104
|
+
rect,
|
|
105
|
+
id: last.l?.value,
|
|
106
|
+
type: last.c ? undefined : last.t?.value.toLowerCase().replace('jpeg', 'jpg'),
|
|
107
|
+
select: Boolean(last.s || last.i || style === 'selection'),
|
|
108
|
+
window: Boolean(last.w || last.W || style === 'window'),
|
|
109
|
+
windowLabel: label('w', 'W') ?? (style === 'window' ? '-J window' : undefined),
|
|
110
|
+
};
|
|
111
|
+
}
|
package/host/tools.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants, statSync, writeSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function say(fd, text) {
|
|
6
|
+
writeSync(fd, text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function runnable(path, platform) {
|
|
10
|
+
try {
|
|
11
|
+
if (!statSync(path).isFile()) return false;
|
|
12
|
+
accessSync(path, platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function dirs(platform, env) {
|
|
20
|
+
const path = (platform === 'win32' ? (env.PATH ?? env.Path) : env.PATH) ?? '';
|
|
21
|
+
return path.split(platform === 'win32' ? ';' : ':').filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function lookup(file, platform, env) {
|
|
25
|
+
for (const dir of dirs(platform, env)) {
|
|
26
|
+
if (runnable(join(dir, file), platform)) return join(dir, file);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function spawned(path, args, { env, stdin = 'ignore', capture = false }) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
let child;
|
|
34
|
+
try {
|
|
35
|
+
child = spawn(path, args, {
|
|
36
|
+
env,
|
|
37
|
+
stdio: [stdin, capture ? 'pipe' : 'ignore', 'pipe'],
|
|
38
|
+
shell: false,
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
resolve({ error });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const out = [];
|
|
46
|
+
const errors = [];
|
|
47
|
+
let code;
|
|
48
|
+
let outClosed = !capture;
|
|
49
|
+
let errClosed = false;
|
|
50
|
+
const settle = () => {
|
|
51
|
+
if (code === undefined || !outClosed) return;
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
child.stderr.destroy();
|
|
54
|
+
resolve({ code, stdout: Buffer.concat(out).toString('utf8') });
|
|
55
|
+
} else if (errClosed) resolve({ code, stderr: Buffer.concat(errors) });
|
|
56
|
+
};
|
|
57
|
+
if (capture) {
|
|
58
|
+
child.stdout.on('data', (chunk) => out.push(chunk));
|
|
59
|
+
child.stdout.on('close', () => {
|
|
60
|
+
outClosed = true;
|
|
61
|
+
settle();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
child.stderr.on('data', (chunk) => errors.push(chunk));
|
|
65
|
+
child.stderr.on('close', () => {
|
|
66
|
+
errClosed = true;
|
|
67
|
+
settle();
|
|
68
|
+
});
|
|
69
|
+
child.on('error', (error) => resolve({ error }));
|
|
70
|
+
child.on('exit', (status) => {
|
|
71
|
+
code = status ?? 1;
|
|
72
|
+
settle();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function step(name, path, args, options, fds) {
|
|
78
|
+
const result = await spawned(path, args, options);
|
|
79
|
+
if (result.error) {
|
|
80
|
+
say(fds[2], `sccapture: ${name}: ${result.error.message}\n`);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (result.code === 0) return result;
|
|
84
|
+
if (result.stderr.length) say(fds[2], result.stderr);
|
|
85
|
+
else say(fds[2], `sccapture: ${name} failed\n`);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
package/host/upgrade.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
2
|
+
import { homedir, tmpdir } from 'node:os';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { dirs, lookup, say, step } from './tools.mjs';
|
|
5
|
+
|
|
6
|
+
const base = 'https://github.com/ai-ecoverse/slicc-capture/releases/latest/download';
|
|
7
|
+
|
|
8
|
+
function version(dir) {
|
|
9
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function home(platform, env) {
|
|
13
|
+
if (platform === 'win32') return join(env.LOCALAPPDATA ?? homedir(), 'slicc-capture');
|
|
14
|
+
return join(env.XDG_DATA_HOME || join(env.HOME ?? homedir(), '.local', 'share'), 'slicc-capture');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function real(path) {
|
|
18
|
+
try {
|
|
19
|
+
return realpathSync(path);
|
|
20
|
+
} catch {
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function launcher(platform, env, data) {
|
|
26
|
+
const fallback =
|
|
27
|
+
platform === 'win32'
|
|
28
|
+
? join(home(platform, env), 'bin')
|
|
29
|
+
: join(env.HOME ?? homedir(), '.local', 'bin');
|
|
30
|
+
const name = platform === 'win32' ? 'sccapture.cmd' : 'sccapture';
|
|
31
|
+
const marks = [data];
|
|
32
|
+
if (real(home(platform, env)) === data) marks.push(home(platform, env));
|
|
33
|
+
for (const dir of [...dirs(platform, env), fallback]) {
|
|
34
|
+
let text;
|
|
35
|
+
try {
|
|
36
|
+
text = readFileSync(join(dir, name), 'utf8');
|
|
37
|
+
} catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (marks.some((mark) => text.includes(mark))) return dir;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function installed(entry) {
|
|
46
|
+
const data = dirname(dirname(dirname(real(entry))));
|
|
47
|
+
if (basename(data) !== 'slicc-capture' || !existsSync(join(data, 'current'))) return undefined;
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function upgrade({ entry, env, platform, fds }) {
|
|
52
|
+
const root = dirname(dirname(entry));
|
|
53
|
+
const old = version(root);
|
|
54
|
+
const data = installed(entry);
|
|
55
|
+
if (!data) {
|
|
56
|
+
say(fds[1], 'npm install -g @ai-ecoverse/slicc-capture@latest\n');
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
const windows = platform === 'win32';
|
|
60
|
+
const getters = windows ? ['curl.exe'] : ['curl', 'wget'];
|
|
61
|
+
const getter = getters
|
|
62
|
+
.map((name) => ({ name, path: lookup(name, platform, env) }))
|
|
63
|
+
.find((g) => g.path);
|
|
64
|
+
if (!getter) {
|
|
65
|
+
say(fds[2], `sccapture: no downloader found (tried ${getters.join(', ')})\n`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
const shells = windows ? ['powershell.exe', 'pwsh.exe'] : ['sh'];
|
|
69
|
+
const shell =
|
|
70
|
+
shells.map((name) => lookup(name, platform, env)).find(Boolean) ??
|
|
71
|
+
(windows ? undefined : '/bin/sh');
|
|
72
|
+
if (!shell) {
|
|
73
|
+
say(fds[2], `sccapture: no PowerShell found (tried ${shells.join(', ')})\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const child = {};
|
|
77
|
+
for (const [key, value] of Object.entries(env)) {
|
|
78
|
+
if (!['VERSION', 'INSTALL_DIR'].includes(key.toUpperCase())) child[key] = value;
|
|
79
|
+
}
|
|
80
|
+
const scratch = mkdtempSync(join(tmpdir(), 'sccapture-upgrade-'));
|
|
81
|
+
try {
|
|
82
|
+
const name = windows ? 'install.ps1' : 'install.sh';
|
|
83
|
+
const script = join(scratch, name);
|
|
84
|
+
const url = `${base}/${name}`;
|
|
85
|
+
const fetch = getter.name === 'wget' ? ['-q', '-O', script, url] : ['-fsSL', '-o', script, url];
|
|
86
|
+
if (!(await step(getter.name, getter.path, fetch, { env: child }, fds))) return 1;
|
|
87
|
+
const dir = launcher(platform, env, data);
|
|
88
|
+
const args = windows
|
|
89
|
+
? [
|
|
90
|
+
'-NoProfile',
|
|
91
|
+
'-NonInteractive',
|
|
92
|
+
'-ExecutionPolicy',
|
|
93
|
+
'Bypass',
|
|
94
|
+
'-File',
|
|
95
|
+
script,
|
|
96
|
+
'--dir',
|
|
97
|
+
dir,
|
|
98
|
+
]
|
|
99
|
+
: [script, '--dir', dir];
|
|
100
|
+
if (!(await step(name, shell, args, { env: child }, fds))) return 1;
|
|
101
|
+
const now = version(join(data, 'current'));
|
|
102
|
+
say(
|
|
103
|
+
fds[1],
|
|
104
|
+
now === old ? `sccapture: already up to date (${old})\n` : `sccapture: ${old} → ${now}\n`
|
|
105
|
+
);
|
|
106
|
+
return 0;
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
package/host/windows.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { lookup, say, step } from './tools.mjs';
|
|
3
|
+
|
|
4
|
+
const extensions = {
|
|
5
|
+
'.png': 'png',
|
|
6
|
+
'.jpg': 'jpg',
|
|
7
|
+
'.jpeg': 'jpg',
|
|
8
|
+
'.bmp': 'bmp',
|
|
9
|
+
'.gif': 'gif',
|
|
10
|
+
'.tif': 'tiff',
|
|
11
|
+
'.tiff': 'tiff',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const native = [
|
|
15
|
+
'using System;',
|
|
16
|
+
'using System.Runtime.InteropServices;',
|
|
17
|
+
'public static class SccaptureNative {',
|
|
18
|
+
'[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }',
|
|
19
|
+
'[StructLayout(LayoutKind.Sequential)] public struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT pt; }',
|
|
20
|
+
'[StructLayout(LayoutKind.Sequential)] public struct ICONINFO { public bool fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; }',
|
|
21
|
+
'[DllImport("user32.dll")] static extern bool SetProcessDPIAware();',
|
|
22
|
+
'[DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr context);',
|
|
23
|
+
'public static void Aware() {',
|
|
24
|
+
'try { if (SetProcessDpiAwarenessContext(new IntPtr(-4))) return; } catch (EntryPointNotFoundException) { }',
|
|
25
|
+
'SetProcessDPIAware();',
|
|
26
|
+
'}',
|
|
27
|
+
'[DllImport("user32.dll")] static extern bool GetCursorInfo(ref CURSORINFO info);',
|
|
28
|
+
'[DllImport("user32.dll")] static extern bool GetIconInfo(IntPtr icon, out ICONINFO info);',
|
|
29
|
+
'[DllImport("user32.dll")] static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr icon, int w, int h, int step, IntPtr brush, int flags);',
|
|
30
|
+
'[DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr handle);',
|
|
31
|
+
'public static void DrawCursor(IntPtr hdc, int left, int top) {',
|
|
32
|
+
'CURSORINFO info = new CURSORINFO();',
|
|
33
|
+
'info.cbSize = Marshal.SizeOf(typeof(CURSORINFO));',
|
|
34
|
+
'if (!GetCursorInfo(ref info) || info.flags != 1) return;',
|
|
35
|
+
'ICONINFO icon;',
|
|
36
|
+
'if (!GetIconInfo(info.hCursor, out icon)) return;',
|
|
37
|
+
'DrawIconEx(hdc, info.pt.X - icon.xHotspot - left, info.pt.Y - icon.yHotspot - top, info.hCursor, 0, 0, 0, IntPtr.Zero, 3);',
|
|
38
|
+
'if (icon.hbmMask != IntPtr.Zero) DeleteObject(icon.hbmMask);',
|
|
39
|
+
'if (icon.hbmColor != IntPtr.Zero) DeleteObject(icon.hbmColor);',
|
|
40
|
+
'}',
|
|
41
|
+
'}',
|
|
42
|
+
].join(' ');
|
|
43
|
+
|
|
44
|
+
export const script = [
|
|
45
|
+
"$ErrorActionPreference = 'Stop'",
|
|
46
|
+
"$ProgressPreference = 'SilentlyContinue'",
|
|
47
|
+
'try {',
|
|
48
|
+
' Add-Type -AssemblyName System.Drawing',
|
|
49
|
+
' Add-Type -AssemblyName System.Windows.Forms',
|
|
50
|
+
` Add-Type -TypeDefinition '${native}'`,
|
|
51
|
+
' [SccaptureNative]::Aware()',
|
|
52
|
+
' $screen = $env:SCCAPTURE_SCREEN',
|
|
53
|
+
" if ($screen -eq 'all') {",
|
|
54
|
+
' $bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen',
|
|
55
|
+
" } elseif ($screen -eq 'primary') {",
|
|
56
|
+
' $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds',
|
|
57
|
+
' } else {',
|
|
58
|
+
' $screens = @([System.Windows.Forms.Screen]::PrimaryScreen) + @([System.Windows.Forms.Screen]::AllScreens | Where-Object { -not $_.Primary })',
|
|
59
|
+
' $index = [int]$screen',
|
|
60
|
+
' if ($index -gt $screens.Count) {',
|
|
61
|
+
" [Console]::Error.WriteLine(('sccapture: -D {0}: no such display' -f $index))",
|
|
62
|
+
' exit 1',
|
|
63
|
+
' }',
|
|
64
|
+
' $bounds = $screens[$index - 1].Bounds',
|
|
65
|
+
' }',
|
|
66
|
+
' if ($env:SCCAPTURE_RECT) {',
|
|
67
|
+
" $rect = [int[]]($env:SCCAPTURE_RECT -split ',')",
|
|
68
|
+
' $bounds = New-Object System.Drawing.Rectangle ($bounds.X + $rect[0]), ($bounds.Y + $rect[1]), $rect[2], $rect[3]',
|
|
69
|
+
' }',
|
|
70
|
+
' $bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height',
|
|
71
|
+
' $graphics = [System.Drawing.Graphics]::FromImage($bitmap)',
|
|
72
|
+
' $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)',
|
|
73
|
+
" if ($env:SCCAPTURE_CURSOR -eq '1') {",
|
|
74
|
+
' $hdc = $graphics.GetHdc()',
|
|
75
|
+
' [SccaptureNative]::DrawCursor($hdc, $bounds.X, $bounds.Y)',
|
|
76
|
+
' $graphics.ReleaseHdc($hdc)',
|
|
77
|
+
' }',
|
|
78
|
+
' $graphics.Dispose()',
|
|
79
|
+
" if ($env:SCCAPTURE_CLIPBOARD -eq '1') {",
|
|
80
|
+
' [System.Windows.Forms.Clipboard]::SetImage($bitmap)',
|
|
81
|
+
' } else {',
|
|
82
|
+
" $formats = @{ png = 'Png'; jpg = 'Jpeg'; bmp = 'Bmp'; gif = 'Gif'; tiff = 'Tiff' }",
|
|
83
|
+
' $bitmap.Save($env:SCCAPTURE_FILE, [System.Drawing.Imaging.ImageFormat]::($formats[$env:SCCAPTURE_FORMAT]))',
|
|
84
|
+
" if ($env:SCCAPTURE_OPEN -eq '1') { Invoke-Item -LiteralPath $env:SCCAPTURE_FILE }",
|
|
85
|
+
' }',
|
|
86
|
+
' $bitmap.Dispose()',
|
|
87
|
+
'} catch {',
|
|
88
|
+
' [Console]::Error.WriteLine($_.Exception.Message)',
|
|
89
|
+
' exit 1',
|
|
90
|
+
'}',
|
|
91
|
+
].join('\n');
|
|
92
|
+
|
|
93
|
+
export const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
94
|
+
|
|
95
|
+
export async function windows({ o, file, env, platform, fds, wait }) {
|
|
96
|
+
const names = ['powershell.exe', 'pwsh.exe'];
|
|
97
|
+
const name = names.find((candidate) => lookup(candidate, platform, env));
|
|
98
|
+
if (!name) {
|
|
99
|
+
say(fds[2], `sccapture: no screenshot tool found (tried ${names.join(', ')})\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
let screen = 'all';
|
|
103
|
+
if (o.display) screen = o.display;
|
|
104
|
+
else if (o.main) screen = 'primary';
|
|
105
|
+
const data = {
|
|
106
|
+
SCCAPTURE_FILE: o.clipboard ? undefined : file,
|
|
107
|
+
SCCAPTURE_FORMAT: o.clipboard
|
|
108
|
+
? 'png'
|
|
109
|
+
: (o.type ?? extensions[extname(file).toLowerCase()] ?? 'png'),
|
|
110
|
+
SCCAPTURE_CLIPBOARD: o.clipboard ? '1' : undefined,
|
|
111
|
+
SCCAPTURE_CURSOR: o.cursor ? '1' : undefined,
|
|
112
|
+
SCCAPTURE_SCREEN: screen,
|
|
113
|
+
SCCAPTURE_RECT: o.rect?.join(','),
|
|
114
|
+
SCCAPTURE_OPEN: o.open && !o.clipboard ? '1' : undefined,
|
|
115
|
+
};
|
|
116
|
+
const child = {};
|
|
117
|
+
for (const [key, value] of Object.entries(env)) {
|
|
118
|
+
if (!key.toUpperCase().startsWith('SCCAPTURE_')) child[key] = value;
|
|
119
|
+
}
|
|
120
|
+
for (const [key, value] of Object.entries(data)) if (value !== undefined) child[key] = value;
|
|
121
|
+
await wait(o.delay);
|
|
122
|
+
const args = ['-NoProfile', '-NonInteractive', '-STA', '-EncodedCommand', encoded];
|
|
123
|
+
const done = await step(name, lookup(name, platform, env), args, { env: child }, fds);
|
|
124
|
+
return done ? 0 : 1;
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-capture",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "screencapture for SLICC, and sccapture for SLICC and any host",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ai-ecoverse/slicc-capture.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"sccapture": "host/sccapture.mjs"
|
|
16
|
+
},
|
|
17
|
+
"fledgling": {
|
|
18
|
+
"provider": "github",
|
|
19
|
+
"workflow": "ci.yml",
|
|
20
|
+
"repo": "ai-ecoverse/slicc-capture",
|
|
21
|
+
"publish": true
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"slicc": {
|
|
28
|
+
"unitSources": [
|
|
29
|
+
"host"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"host"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prepare": "slicc-install-hooks",
|
|
37
|
+
"lint": "slicc-lint",
|
|
38
|
+
"format": "biome check --write .",
|
|
39
|
+
"test": "node --test --test-timeout=120000 test/integration/*.test.mjs",
|
|
40
|
+
"test:unit": "mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'",
|
|
41
|
+
"build": "npm pack --dry-run",
|
|
42
|
+
"trust": "npx fledgling sync -y --skip-publish",
|
|
43
|
+
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
|
|
44
|
+
"release": "semantic-release"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@ai-ecoverse/slicc-shared-web": "2.11.3",
|
|
48
|
+
"@biomejs/biome": "2.5.15",
|
|
49
|
+
"lefthook": "2.2.2",
|
|
50
|
+
"monocart-coverage-reports": "2.13.2",
|
|
51
|
+
"semantic-release": "25.0.9"
|
|
52
|
+
}
|
|
53
|
+
}
|