macvscr 0.1.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 +21 -0
- package/README.md +112 -0
- package/bin/vscr.js +318 -0
- package/binaries/vscr-darwin-arm64 +0 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kzf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# vscr
|
|
2
|
+
|
|
3
|
+
A macOS menu-bar tool that creates controllable **virtual displays** — arbitrary resolution, HiDPI (Retina `@2x`), aspect ratio, and iMac presets. Unblocks HiDPI screen-sharing on headless Macs (e.g. a Mac mini with no monitor attached).
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
When you screen-share into a headless Mac, macOS locks the session to a 1920×1080 non-HiDPI surface. `vscr` creates a dummy display at any resolution + Retina, which Screen Sharing then renders.
|
|
8
|
+
|
|
9
|
+
## Resolution model
|
|
10
|
+
|
|
11
|
+
Everything you type and see is in **logical pixels**; HiDPI (`@2x`) means the physical surface is doubled for crispness.
|
|
12
|
+
|
|
13
|
+
- `2560×1440 @2x` → logical workspace 2560×1440, physical 5120×2880 (the Retina way macOS itself describes 5K iMacs)
|
|
14
|
+
- `1920×1080` (HiDPI off) → logical = physical = 1920×1080
|
|
15
|
+
|
|
16
|
+
The tray header shows both, so logical and physical are never confused.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
Zero-install, one line (recommended):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx -y vscr@latest setup
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`setup` copies a stable binary to `~/.vscr/vscr`, adds `alias vscr='npx -y vscr@latest'` to your shell rc (if you don't already have a `vscr` command), and registers a login LaunchAgent so the tray app runs at login and restarts on crash. It prints a `source` reminder and how to stop it.
|
|
27
|
+
|
|
28
|
+
Or install globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g vscr
|
|
32
|
+
vscr setup
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires macOS 13+ on Apple Silicon (arm64).
|
|
36
|
+
|
|
37
|
+
## Control the service
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
vscr status # loaded? running? pid?
|
|
41
|
+
vscr stop # stop (returns at next login, or `vscr start`)
|
|
42
|
+
vscr start # start again
|
|
43
|
+
vscr restart # restart
|
|
44
|
+
vscr uninstall # full teardown: LaunchAgent + ~/.vscr + shell alias
|
|
45
|
+
vscr --help # everything
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Stop the app for now without removing the service: click the **display icon in the menu bar → 退出**, or `vscr stop`. A clean quit is **not** restarted; only a crash is.
|
|
49
|
+
|
|
50
|
+
## Run directly (one-off)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
vscr # launch the menu-bar app now
|
|
54
|
+
vscr --width 2560 --ratio 16:9 --hidpi
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Once running, click the display icon in the menu bar:
|
|
58
|
+
|
|
59
|
+
- **常见分辨率 ▸** — presets; iMac sizes are folded in with a tail (`2048×1152 @2x · iMac 21.5"` …).
|
|
60
|
+
- **宽度 ▸** / **高度 ▸** / **比例 ▸** — each has quick picks **and a `自定义…`** entry that opens an input dialog.
|
|
61
|
+
- Width, height, and ratio are linked: width is the master axis; set height or ratio and the other adjusts.
|
|
62
|
+
- **HiDPI / Retina @2x** — toggle Retina backing.
|
|
63
|
+
- **退出** — quit the app (clean exit; the service won't relaunch it until next login or `vscr start`).
|
|
64
|
+
|
|
65
|
+
The header always shows the active logical resolution (`@2x` when Retina) plus the physical size; the active item is checked. Switching rebuilds the display live — no restart.
|
|
66
|
+
|
|
67
|
+
### CLI flags (all pixel values are logical)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
--width <px> logical width
|
|
71
|
+
--height <px> logical height (mutually exclusive with --ratio)
|
|
72
|
+
--ratio <16:9|16:10|21:9|32:9|4:3|3:2|5:4|1:1>
|
|
73
|
+
--hidpi / --no-hidpi Retina @2x on/off (default on)
|
|
74
|
+
--refresh <Hz> refresh rate (default 60)
|
|
75
|
+
--name <name> display name prefix
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Notes on the daemon
|
|
79
|
+
|
|
80
|
+
- A menu-bar app needs your GUI session, so it starts at **login** (LaunchAgent), not boot (a LaunchDaemon has no GUI).
|
|
81
|
+
- The LaunchAgent runs the stable `~/.vscr/vscr` directly — no `npx`/network at login. The `vscr` command (alias) still uses npx so you always get subcommands + the latest version.
|
|
82
|
+
- Logs: `/tmp/vscr.out.log`, `/tmp/vscr.err.log`.
|
|
83
|
+
|
|
84
|
+
## Headless / Screen Sharing
|
|
85
|
+
|
|
86
|
+
1. On the headless Mac, run `vscr setup` (or `npx -y vscr@latest setup`) and pick a preset from the menu bar.
|
|
87
|
+
2. From another Mac, connect via Screen Sharing, then in the **View** menu select the dummy.
|
|
88
|
+
3. The remote session renders at the chosen geometry with Retina.
|
|
89
|
+
|
|
90
|
+
## Verify
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
system_profiler SPDisplaysDataType | grep -i virtual
|
|
94
|
+
# → Virtual Display <physical W>x<physical H> Resolution: <W> x <H>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Notes
|
|
98
|
+
|
|
99
|
+
- The virtual display is owned by the process and is destroyed when `vscr` quits.
|
|
100
|
+
- Uses the macOS private `CGVirtualDisplay*` SPI (in the public CoreGraphics binary). SIP stays on; the app is non-sandboxed.
|
|
101
|
+
- MIT licensed.
|
|
102
|
+
|
|
103
|
+
## Build from source
|
|
104
|
+
|
|
105
|
+
This package bundles a prebuilt universal macOS binary. To rebuild from the Swift source:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/kzf/macos_virtual_screen
|
|
109
|
+
cd macos_virtual_screen
|
|
110
|
+
swift build -c release --arch arm64
|
|
111
|
+
cp .build/release/VirtualScreen npm/binaries/vscr-darwin-arm64
|
|
112
|
+
```
|
package/bin/vscr.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// vscr CLI — launches the native macOS menu-bar binary and manages a user
|
|
5
|
+
// LaunchAgent (login start + crash restart = the "daemon" mode).
|
|
6
|
+
//
|
|
7
|
+
// `vscr setup` is the standard one-liner (`npx -y vscr@latest setup`):
|
|
8
|
+
// 1. copies the bundled binary to a stable path (~/.vscr/vscr) for the daemon
|
|
9
|
+
// 2. adds `alias vscr='npx -y vscr@latest'` to the shell rc if no vscr command exists
|
|
10
|
+
// 3. registers + starts the LaunchAgent (runs at login, restarts on crash)
|
|
11
|
+
//
|
|
12
|
+
// Why a LaunchAgent (login) not a LaunchDaemon (boot): a menu-bar tray app
|
|
13
|
+
// needs the user's GUI session (WindowServer), which exists only after login.
|
|
14
|
+
|
|
15
|
+
const { spawn, spawnSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const LABEL = 'npm.vscr';
|
|
21
|
+
const BIN = path.join(__dirname, '..', 'binaries', 'vscr-darwin-arm64');
|
|
22
|
+
const AGENT_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
23
|
+
const PLIST = path.join(AGENT_DIR, `${LABEL}.plist`);
|
|
24
|
+
const STABLE_DIR = path.join(os.homedir(), '.vscr');
|
|
25
|
+
const STABLE_BIN = path.join(STABLE_DIR, 'vscr');
|
|
26
|
+
|
|
27
|
+
if (process.platform !== 'darwin') {
|
|
28
|
+
console.error('vscr: macOS only.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (!fs.existsSync(BIN)) {
|
|
32
|
+
console.error('vscr: native binary not found at ' + BIN);
|
|
33
|
+
console.error(' The package looks corrupt — try reinstalling it.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const uid = process.getuid();
|
|
38
|
+
const domainTarget = `gui/${uid}`;
|
|
39
|
+
const serviceTarget = `gui/${uid}/${LABEL}`;
|
|
40
|
+
|
|
41
|
+
// --- helpers ---
|
|
42
|
+
|
|
43
|
+
function sh(file, args) {
|
|
44
|
+
return spawnSync(file, args, { stdio: 'inherit' });
|
|
45
|
+
}
|
|
46
|
+
function shOut(file, args) {
|
|
47
|
+
return spawnSync(file, args, { encoding: 'utf8' });
|
|
48
|
+
}
|
|
49
|
+
function escapeXML(s) {
|
|
50
|
+
return String(s).replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
51
|
+
}
|
|
52
|
+
function isLoaded() {
|
|
53
|
+
return shOut('launchctl', ['print', serviceTarget]).status === 0;
|
|
54
|
+
}
|
|
55
|
+
function servicePid() {
|
|
56
|
+
if (!isLoaded()) return null;
|
|
57
|
+
const r = shOut('launchctl', ['print', serviceTarget]);
|
|
58
|
+
const m = r.stdout && r.stdout.match(/\bpid\s*=\s*(\d+)/);
|
|
59
|
+
return m ? m[1] : null;
|
|
60
|
+
}
|
|
61
|
+
function hasVscrCommand() {
|
|
62
|
+
return shOut('sh', ['-c', 'command -v vscr']).status === 0;
|
|
63
|
+
}
|
|
64
|
+
function rcFile() {
|
|
65
|
+
const shell = process.env.SHELL || '';
|
|
66
|
+
if (shell.endsWith('bash')) return path.join(os.homedir(), '.bashrc');
|
|
67
|
+
return path.join(os.homedir(), '.zshrc'); // macOS default + fallback
|
|
68
|
+
}
|
|
69
|
+
// The alias block is wrapped in stable sentinel markers so future changes to
|
|
70
|
+
// the alias command itself never break detection/removal — we match the
|
|
71
|
+
// markers, not the inner command.
|
|
72
|
+
const ALIAS_HEAD = '# +++npm:vscr';
|
|
73
|
+
const ALIAS_TAIL = '# ---npm:vscr';
|
|
74
|
+
const ALIAS_LINE = `alias vscr='npx -y vscr@latest'`;
|
|
75
|
+
const ALIAS_BLOCK = `\n${ALIAS_HEAD}\n# managed by vscr setup — remove with \`vscr uninstall\`\n${ALIAS_LINE}\n${ALIAS_TAIL}\n`;
|
|
76
|
+
const ALIAS_BLOCK_RE = /\n# \+\+\+npm:vscr[\s\S]*?# ---npm:vscr\n?/;
|
|
77
|
+
|
|
78
|
+
function aliasInRc() {
|
|
79
|
+
const rc = rcFile();
|
|
80
|
+
try {
|
|
81
|
+
return fs.existsSync(rc) && fs.readFileSync(rc, 'utf8').includes(ALIAS_HEAD);
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function appendAliasBlock() {
|
|
87
|
+
const rc = rcFile();
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(rc)) fs.appendFileSync(rc, ALIAS_BLOCK);
|
|
90
|
+
else fs.writeFileSync(rc, ALIAS_BLOCK);
|
|
91
|
+
return true;
|
|
92
|
+
} catch (_) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function removeAlias() {
|
|
97
|
+
const rc = rcFile();
|
|
98
|
+
if (!fs.existsSync(rc)) return false;
|
|
99
|
+
try {
|
|
100
|
+
const content = fs.readFileSync(rc, 'utf8');
|
|
101
|
+
if (!ALIAS_BLOCK_RE.test(content)) return false;
|
|
102
|
+
fs.writeFileSync(rc, content.replace(ALIAS_BLOCK_RE, ''));
|
|
103
|
+
return true;
|
|
104
|
+
} catch (_) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reconcile the shell alias with reality so it never shadows a real command
|
|
110
|
+
* and never goes stale:
|
|
111
|
+
* - global `vscr` on PATH -> remove any alias (it would shadow the command)
|
|
112
|
+
* - no global `vscr` -> ensure the alias exists
|
|
113
|
+
* Returns { note, newlyAdded } for setup messaging.
|
|
114
|
+
*/
|
|
115
|
+
function syncAlias() {
|
|
116
|
+
const rc = rcFile();
|
|
117
|
+
const present = aliasInRc();
|
|
118
|
+
if (hasVscrCommand()) {
|
|
119
|
+
if (present) {
|
|
120
|
+
removeAlias();
|
|
121
|
+
return { note: `${rc} (alias removed — global vscr on PATH; alias would shadow it)`, newlyAdded: false };
|
|
122
|
+
}
|
|
123
|
+
return { note: 'skipped (global vscr on PATH)', newlyAdded: false };
|
|
124
|
+
}
|
|
125
|
+
if (present) return { note: `${rc} (already present)`, newlyAdded: false };
|
|
126
|
+
const ok = appendAliasBlock();
|
|
127
|
+
return { note: ok ? `${rc} (added)` : `(could not write ${rc})`, newlyAdded: ok };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function plistXML(daemonBin, args) {
|
|
131
|
+
const progArgs = [daemonBin, ...args].map(a => ` <string>${escapeXML(a)}</string>`).join('\n');
|
|
132
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
133
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
134
|
+
<plist version="1.0">
|
|
135
|
+
<dict>
|
|
136
|
+
<key>Label</key>
|
|
137
|
+
<string>${LABEL}</string>
|
|
138
|
+
<key>ProgramArguments</key>
|
|
139
|
+
<array>
|
|
140
|
+
${progArgs}
|
|
141
|
+
</array>
|
|
142
|
+
<key>RunAtLoad</key>
|
|
143
|
+
<true/>
|
|
144
|
+
<key>KeepAlive</key>
|
|
145
|
+
<dict>
|
|
146
|
+
<key>SuccessfulExit</key>
|
|
147
|
+
<false/>
|
|
148
|
+
</dict>
|
|
149
|
+
<key>ProcessType</key>
|
|
150
|
+
<string>Interactive</string>
|
|
151
|
+
<key>StandardOutPath</key>
|
|
152
|
+
<string>/tmp/vscr.out.log</string>
|
|
153
|
+
<key>StandardErrorPath</key>
|
|
154
|
+
<string>/tmp/vscr.err.log</string>
|
|
155
|
+
</dict>
|
|
156
|
+
</plist>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Write the plist (pointing at daemonBin) and bootstrap it. */
|
|
161
|
+
function bootstrapService(daemonBin, args) {
|
|
162
|
+
fs.mkdirSync(AGENT_DIR, { recursive: true });
|
|
163
|
+
if (isLoaded()) sh('launchctl', ['bootout', serviceTarget]);
|
|
164
|
+
fs.writeFileSync(PLIST, plistXML(daemonBin, args));
|
|
165
|
+
const r = sh('launchctl', ['bootstrap', domainTarget, PLIST]);
|
|
166
|
+
if (r.status !== 0) {
|
|
167
|
+
console.error('vscr: failed to bootstrap the LaunchAgent.');
|
|
168
|
+
process.exit(r.status || 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- commands ---
|
|
173
|
+
|
|
174
|
+
function setup(args) {
|
|
175
|
+
console.log('vscr: setting up…');
|
|
176
|
+
|
|
177
|
+
// 1. stable daemon binary (so login launch needs no network / no npx)
|
|
178
|
+
fs.mkdirSync(STABLE_DIR, { recursive: true });
|
|
179
|
+
fs.copyFileSync(BIN, STABLE_BIN);
|
|
180
|
+
fs.chmodSync(STABLE_BIN, 0o755);
|
|
181
|
+
|
|
182
|
+
// 2. shell convenience: smart alias (add if no vscr; remove if global vscr would be shadowed)
|
|
183
|
+
const { note: aliasNote, newlyAdded: addedAlias } = syncAlias();
|
|
184
|
+
|
|
185
|
+
// 3. register + start the LaunchAgent, pointing at the stable binary
|
|
186
|
+
bootstrapService(STABLE_BIN, args);
|
|
187
|
+
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('vscr: setup complete.');
|
|
190
|
+
console.log(` daemon binary: ${STABLE_BIN}`);
|
|
191
|
+
console.log(` LaunchAgent: ${PLIST} (runs at login; restarts on crash)`);
|
|
192
|
+
console.log(` shell alias: ${aliasNote}`);
|
|
193
|
+
if (addedAlias) {
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(' ▸ The `vscr` command won\'t exist in THIS terminal until you reload the shell:');
|
|
196
|
+
console.log(` source ${rcFile()}`);
|
|
197
|
+
}
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log(' How to control vscr:');
|
|
200
|
+
console.log(' • Quit the app now: click the display icon in the menu bar → 退出');
|
|
201
|
+
console.log(' • Stop the service: vscr stop');
|
|
202
|
+
console.log(' • Full teardown: vscr uninstall');
|
|
203
|
+
console.log(' • All options: vscr --help');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function uninstall() {
|
|
207
|
+
if (isLoaded()) sh('launchctl', ['bootout', serviceTarget]);
|
|
208
|
+
if (fs.existsSync(PLIST)) fs.unlinkSync(PLIST);
|
|
209
|
+
if (fs.existsSync(STABLE_DIR)) fs.rmSync(STABLE_DIR, { recursive: true, force: true });
|
|
210
|
+
removeAlias();
|
|
211
|
+
sh('pkill', ['-f', STABLE_BIN]);
|
|
212
|
+
sh('pkill', ['-f', BIN]);
|
|
213
|
+
console.log('vscr: uninstalled (LaunchAgent, ~/.vscr, and shell alias removed).');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function start() {
|
|
217
|
+
if (!fs.existsSync(PLIST)) {
|
|
218
|
+
console.error('vscr: not installed. Run `vscr setup` first.');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
if (isLoaded()) {
|
|
222
|
+
sh('launchctl', ['kickstart', '-k', serviceTarget]);
|
|
223
|
+
} else {
|
|
224
|
+
sh('launchctl', ['bootstrap', domainTarget, PLIST]);
|
|
225
|
+
}
|
|
226
|
+
console.log('vscr: started.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function stop() {
|
|
230
|
+
if (isLoaded()) sh('launchctl', ['bootout', serviceTarget]);
|
|
231
|
+
sh('pkill', ['-f', STABLE_BIN]);
|
|
232
|
+
sh('pkill', ['-f', BIN]);
|
|
233
|
+
console.log('vscr: stopped. (Returns at next login, or run `vscr start`.)');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function status() {
|
|
237
|
+
if (!fs.existsSync(PLIST)) {
|
|
238
|
+
console.log('vscr: not installed.');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!isLoaded()) {
|
|
242
|
+
console.log('vscr: installed (plist present) but not loaded in this session.');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const r = shOut('launchctl', ['print', serviceTarget]);
|
|
246
|
+
const pid = servicePid();
|
|
247
|
+
const lastM = r.stdout && r.stdout.match(/last exit code = (\d+)/);
|
|
248
|
+
console.log(`vscr: loaded (${LABEL}).`);
|
|
249
|
+
console.log(` ${pid ? `running, pid ${pid}` : 'not currently running'}`);
|
|
250
|
+
if (lastM) console.log(` last exit code: ${lastM[1]}`);
|
|
251
|
+
console.log(` plist: ${PLIST}`);
|
|
252
|
+
console.log(` daemon binary: ${fs.existsSync(STABLE_BIN) ? STABLE_BIN : '(~/.vscr/vscr missing — run vscr setup)'}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function runApp(args) {
|
|
256
|
+
const pid = servicePid();
|
|
257
|
+
if (pid) {
|
|
258
|
+
console.log(`vscr: already running as a login service (pid ${pid}). Use the menu-bar icon.`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const wantsHelp = args.some(a => a === '-h' || a === '--help' || a === '--version' || a === '-v');
|
|
262
|
+
if (wantsHelp) {
|
|
263
|
+
const r = spawnSync(BIN, args, { stdio: 'inherit' });
|
|
264
|
+
process.exit(r.status == null ? 1 : r.status);
|
|
265
|
+
}
|
|
266
|
+
const child = spawn(BIN, args, { detached: true, stdio: 'ignore' });
|
|
267
|
+
child.on('error', err => {
|
|
268
|
+
console.error('vscr: failed to launch:', err.message);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
child.unref();
|
|
272
|
+
console.log(`vscr: running (pid ${child.pid}). Look for the display icon in the menu bar.`);
|
|
273
|
+
console.log(' For always-on: `vscr setup`. Manage: `vscr status | start | stop | uninstall`.');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function printHelp() {
|
|
277
|
+
console.log(`vscr — macOS virtual display tray tool
|
|
278
|
+
|
|
279
|
+
Standard setup (recommended):
|
|
280
|
+
npx -y vscr@latest setup [--width N --ratio W:H ...]
|
|
281
|
+
copies a stable binary, adds a shell alias if needed, and registers +
|
|
282
|
+
starts a login LaunchAgent (runs at login, restarts on crash).
|
|
283
|
+
|
|
284
|
+
Manage the service:
|
|
285
|
+
vscr status loaded? running? pid?
|
|
286
|
+
vscr start start the service
|
|
287
|
+
vscr stop stop (returns at next login, or 'vscr start')
|
|
288
|
+
vscr restart restart
|
|
289
|
+
vscr uninstall remove the LaunchAgent, ~/.vscr, and the shell alias
|
|
290
|
+
|
|
291
|
+
Run directly (one-off):
|
|
292
|
+
vscr [resolution flags] launch the menu-bar app now
|
|
293
|
+
|
|
294
|
+
A menu-bar app needs your GUI session, so it starts at LOGIN, not boot.
|
|
295
|
+
Resolution flags (all pixels are LOGICAL; @2x = physical × 2):`);
|
|
296
|
+
const r = spawnSync(BIN, ['--help'], { stdio: 'inherit' });
|
|
297
|
+
if (r.status) process.exit(r.status);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- dispatch ---
|
|
301
|
+
|
|
302
|
+
const sub = process.argv[2];
|
|
303
|
+
const rest = process.argv.slice(3).map(String);
|
|
304
|
+
|
|
305
|
+
switch (sub) {
|
|
306
|
+
case 'setup':
|
|
307
|
+
case 'install': setup(rest); break;
|
|
308
|
+
case 'uninstall': uninstall(); break;
|
|
309
|
+
case 'start': start(); break;
|
|
310
|
+
case 'stop': stop(); break;
|
|
311
|
+
case 'restart': stop(); setTimeout(start, 700); break;
|
|
312
|
+
case 'status': status(); break;
|
|
313
|
+
case 'help':
|
|
314
|
+
case '-h':
|
|
315
|
+
case '--help': printHelp(); break;
|
|
316
|
+
case undefined: runApp([]); break;
|
|
317
|
+
default: runApp(process.argv.slice(2)); break;
|
|
318
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "macvscr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "macOS menu-bar tool that creates controllable virtual displays — arbitrary resolution, HiDPI, aspect ratio, and iMac presets. Unblocks HiDPI screen-sharing on headless Macs.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"vscr": "bin/vscr.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"binaries/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"arm64"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=14"
|
|
22
|
+
},
|
|
23
|
+
"preferGlobal": true,
|
|
24
|
+
"keywords": [
|
|
25
|
+
"macos",
|
|
26
|
+
"virtual-display",
|
|
27
|
+
"dummy-display",
|
|
28
|
+
"menubar",
|
|
29
|
+
"tray",
|
|
30
|
+
"hidpi",
|
|
31
|
+
"retina",
|
|
32
|
+
"resolution",
|
|
33
|
+
"headless",
|
|
34
|
+
"screen-sharing",
|
|
35
|
+
"mac-mini",
|
|
36
|
+
"imac",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "kzf",
|
|
41
|
+
"homepage": "https://github.com/kzf/macos_virtual_screen",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/kzf/macos_virtual_screen.git",
|
|
45
|
+
"directory": "npm"
|
|
46
|
+
}
|
|
47
|
+
}
|