Skip to content

Commit 0b7ce40

Browse files
committedDec 6, 2020
feat: add displays.mirror()
1 parent f9d4198 commit 0b7ce40

File tree

5 files changed

+144
-9
lines changed

5 files changed

+144
-9
lines changed
 

‎README.md

+17
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,23 @@ console.log(display)
139139
*/
140140
```
141141

142+
### `displays.mirror(enable, firstID[, secondID])`
143+
144+
* `enable` Boolean - Whether to enable or disable mirroring.
145+
* `firstID` Number - The device ID for the primary display. If no second display ID is provided, the first
146+
display will default to the system's main display and the display corresponding to this ID will be set as the mirroring display.
147+
* `secondID` Number (optional) - The device ID for the secondary display (which will mirror the first).
148+
149+
Example Usage:
150+
```js
151+
const displays = require('node-mac-displays')
152+
153+
const [firstDisplay, secondDisplay] = displays.getAllDisplays()
154+
155+
// Set the second display to mirror the first.
156+
displays.mirror(true, firstDisplay.id, secondDisplay.id)
157+
```
158+
142159
### `displays.screenshot(id, options)`
143160

144161
* `id` Number - The device ID for the display.

‎displays.mm

+75
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@
44

55
/***** HELPERS *****/
66

7+
// Converts a CGError to a human-readable string.
8+
std::string CGErrorToString(CGError err) {
9+
if (err == kCGErrorCannotComplete) {
10+
return "The requested operation is inappropriate for the parameters passed "
11+
"in, or the current system state";
12+
} else if (err == kCGErrorFailure) {
13+
return "A general failure occurred";
14+
} else if (err == kCGErrorIllegalArgument) {
15+
return "One or more of the parameters passed to a function are invalid";
16+
} else if (err == kCGErrorInvalidConnection) {
17+
return "The parameter representing a connection to the window server is "
18+
"invalid";
19+
} else if (err == kCGErrorInvalidContext) {
20+
return "The CPSProcessSerNum or context identifier parameter is not valid";
21+
} else if (err == kCGErrorInvalidOperation) {
22+
return "The requested operation is not valid for the parameters passed in, "
23+
"or the current system state.";
24+
} else if (err == kCGErrorNoneAvailable) {
25+
return "The requested operation could not be completed as the indicated "
26+
"resources were not found";
27+
} else if (err == kCGErrorNotImplemented) {
28+
return "Return value from obsolete function stubs present for binary "
29+
"compatibility, but not typically called";
30+
} else if (err == kCGErrorRangeCheck) {
31+
return "A parameter passed in has a value that is inappropriate, or which "
32+
"does not map to a useful operation or value";
33+
} else if (err == kCGErrorTypeCheck) {
34+
return "A data type or token was encountered that did not match the "
35+
"expected type or token";
36+
} else {
37+
return "Unknown display configuration error";
38+
}
39+
}
40+
741
// Converts a simple C array to a Napi::Array.
842
template <typename T> Napi::Array CArrayToNapiArray(Napi::Env env, T *c_arr) {
943
Napi::Array arr = Napi::Array::New(env, sizeof c_arr);
@@ -191,6 +225,45 @@ bool GetIsMonochrome() {
191225
return Napi::Object();
192226
}
193227

228+
// Configure two macOS system displays to mirror status.
229+
void Mirror(const Napi::CallbackInfo &info) {
230+
bool enable = info[0].As<Napi::Boolean>().Value();
231+
232+
uint32_t primary_display_id;
233+
uint32_t secondary_display_id;
234+
if (info.Length() == 3) {
235+
primary_display_id = info[1].As<Napi::Number>().Uint32Value();
236+
secondary_display_id = info[2].As<Napi::Number>().Uint32Value();
237+
} else {
238+
primary_display_id = CGMainDisplayID();
239+
secondary_display_id = info[1].As<Napi::Number>().Uint32Value();
240+
}
241+
242+
CGDisplayConfigRef config_ref;
243+
CGError err = CGBeginDisplayConfiguration(&config_ref);
244+
if (err != 0) {
245+
Napi::Error::New(info.Env(), CGErrorToString(err))
246+
.ThrowAsJavaScriptException();
247+
return;
248+
}
249+
250+
err = CGConfigureDisplayMirrorOfDisplay(config_ref, secondary_display_id,
251+
enable ? primary_display_id
252+
: kCGNullDirectDisplay);
253+
if (err != 0) {
254+
Napi::Error::New(info.Env(), CGErrorToString(err))
255+
.ThrowAsJavaScriptException();
256+
return;
257+
}
258+
259+
err = CGCompleteDisplayConfiguration(config_ref, kCGConfigurePermanently);
260+
if (err != 0) {
261+
Napi::Error::New(info.Env(), CGErrorToString(err))
262+
.ThrowAsJavaScriptException();
263+
return;
264+
}
265+
}
266+
194267
// Initializes all functions exposed to JS.
195268
Napi::Object Init(Napi::Env env, Napi::Object exports) {
196269
exports.Set(Napi::String::New(env, "getAllDisplays"),
@@ -199,6 +272,8 @@ bool GetIsMonochrome() {
199272
Napi::Function::New(env, GetPrimaryDisplay));
200273
exports.Set(Napi::String::New(env, "getDisplayFromID"),
201274
Napi::Function::New(env, GetDisplayFromID));
275+
exports.Set(Napi::String::New(env, "mirror"),
276+
Napi::Function::New(env, Mirror));
202277
exports.Set(Napi::String::New(env, "screenshot"),
203278
Napi::Function::New(env, Screenshot));
204279

‎index.js

+24-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ function getDisplayFromID(id) {
88
return displays.getDisplayFromID.call(this, id)
99
}
1010

11+
function mirror(enable, firstID, secondID) {
12+
if (typeof enable !== 'boolean') {
13+
throw new TypeError(`'enable' must be a boolean`)
14+
}
15+
16+
if (typeof firstID !== 'number') {
17+
throw new TypeError(`'firstID' must be a number`)
18+
}
19+
20+
if (secondID && typeof secondID !== 'number') {
21+
throw new TypeError(`'secondID' must be a number`)
22+
}
23+
24+
displays.mirror.call(this, enable, firstID, secondID)
25+
}
26+
1127
function screenshot(id, options = {}) {
1228
if (typeof id !== 'number') {
1329
throw new TypeError(`'id' must be a number`)
@@ -16,23 +32,23 @@ function screenshot(id, options = {}) {
1632
const validTypes = ['jpeg', 'tiff', 'png']
1733
if (options.type) {
1834
if (typeof options.type !== 'string') {
19-
throw new Error(`'type' must be a string`)
35+
throw new TypeError(`'type' must be a string`)
2036
} else if (!validTypes.includes(options.type)) {
21-
throw new Error(`'type' must be one of ${validTypes.join(', ')}`)
37+
throw new TypeError(`'type' must be one of ${validTypes.join(', ')}`)
2238
}
2339
}
2440

2541
if (options.bounds) {
2642
if (typeof options.bounds !== 'object') {
27-
throw new Error(`'bounds' must be a number`)
43+
throw new TypeError(`'bounds' must be a number`)
2844
} else if (!options.bounds.x || typeof options.bounds.x !== 'number') {
29-
throw new Error(`'bounds.x' must be a number`)
45+
throw new TypeError(`'bounds.x' must be a number`)
3046
} else if (!options.bounds.y || typeof options.bounds.y !== 'number') {
31-
throw new Error(`'bounds.y' must be a number`)
47+
throw new TypeError(`'bounds.y' must be a number`)
3248
} else if (!options.bounds.width || typeof options.bounds.width !== 'number') {
33-
throw new Error(`'bounds.width' must be a number`)
49+
throw new TypeError(`'bounds.width' must be a number`)
3450
} else if (!options.bounds.height || typeof options.bounds.height !== 'number') {
35-
throw new Error(`'bounds.height' must be a number`)
51+
throw new TypeError(`'bounds.height' must be a number`)
3652
}
3753
}
3854

@@ -43,5 +59,6 @@ module.exports = {
4359
getAllDisplays: displays.getAllDisplays,
4460
getPrimaryDisplay: displays.getPrimaryDisplay,
4561
getDisplayFromID,
62+
mirror,
4663
screenshot,
4764
}

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"version": "1.0.0",
44
"description": "A native Node.js module for enumerating over system display information",
55
"main": "index.js",
6-
"types": "index.d.ts",
76
"scripts": {
87
"build": "node-gyp rebuild",
98
"clean": "node-gyp clean",

‎test/module.spec.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
const { expect } = require('chai')
22
const fs = require('fs')
33
const path = require('path')
4-
const { getAllDisplays, getPrimaryDisplay, getDisplayFromID, screenshot } = require('../index')
4+
const {
5+
getAllDisplays,
6+
getPrimaryDisplay,
7+
getDisplayFromID,
8+
mirror,
9+
screenshot,
10+
} = require('../index')
511

612
describe('node-mac-displays', () => {
713
describe('getAllDisplays', () => {
@@ -119,6 +125,27 @@ describe('node-mac-displays', () => {
119125
})
120126
})
121127

128+
describe('mirror', () => {
129+
it('throws an error if enable is not a boolean', () => {
130+
expect(() => {
131+
mirror('im a string!!')
132+
}).to.throw(`'enable' must be a boolean`)
133+
})
134+
135+
it('throws an error if firstID is not valid', () => {
136+
expect(() => {
137+
mirror(true, 'bad-type')
138+
}).to.throw(`'firstID' must be a number`)
139+
})
140+
141+
it('throws an error if options.type is not valid', () => {
142+
expect(() => {
143+
const { id } = getPrimaryDisplay()
144+
mirror(true, id, 'bad-type')
145+
}).to.throw(`'secondID' must be a number`)
146+
})
147+
})
148+
122149
describe('screenshot', () => {
123150
it('throws an error if id is not a number', () => {
124151
expect(() => {

0 commit comments

Comments
 (0)
Failed to load comments.