Skip to content

Commit 4741e2d

Browse files
committed
Add macOS Terminal export
1 parent c52c75e commit 4741e2d

9 files changed

Lines changed: 424 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Put the generated file to `~/.kde/share/apps/konsole/NAME-OF-SCHEME.colorscheme`
3131
* __iTerm2 for Mac:__
3232
Create a file `~/NAME-OF-SCHEME.itermcolors` with the generated XML content and load it with the `Load Presets...` button under `iTerm2 / Preferences / Profiles / <Your Profile> / Colors`.
3333

34+
* __macOS Terminal.app:__
35+
Open the generated `.terminal` file, then manage it under `Terminal / Settings / Profiles`.
36+
3437
* __Putty:__
3538
Save the generated file with `.reg` extension and double click it.
3639

src/infrastructure/serialization/scheme-exporters.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { serializeGuake } from './scheme-exports/guake';
66
import { serializeITerm2 } from './scheme-exports/iterm2';
77
import { serializeKitty } from './scheme-exports/kitty';
88
import { serializeKonsole } from './scheme-exports/konsole';
9+
import { serializeMacosTerminal } from './scheme-exports/macos-terminal/index';
910
import { serializeMintty } from './scheme-exports/mintty';
1011
import { serializePutty } from './scheme-exports/putty';
1112
import { serializeTermite } from './scheme-exports/termite';
@@ -16,6 +17,7 @@ import { serializeXresources } from './scheme-exports/xresources';
1617

1718
const TEXT_MIME_TYPE = 'text/plain;charset=utf-8';
1819
const JSON_MIME_TYPE = 'application/json;charset=utf-8';
20+
const PLIST_MIME_TYPE = 'application/x-plist;charset=utf-8';
1921
const XML_MIME_TYPE = 'application/xml;charset=utf-8';
2022

2123
const EXPORT_BUILDERS = {
@@ -26,6 +28,7 @@ const EXPORT_BUILDERS = {
2628
gnomeTerminal: serializeGnomeTerminal,
2729
kitty: serializeKitty,
2830
konsole: serializeKonsole,
31+
macosTerminal: serializeMacosTerminal,
2932
iTerm2: serializeITerm2,
3033
xfceTerminal: serializeXfceTerminal,
3134
mintty: serializeMintty,
@@ -100,6 +103,14 @@ export const SCHEME_DOWNLOADS = [
100103
downloadName: '4bit.colorscheme',
101104
mimeType: TEXT_MIME_TYPE,
102105
},
106+
{
107+
id: 'macosTerminal',
108+
buttonId: 'macos-terminal-button',
109+
text: 'macOS Terminal.app',
110+
linkLabel: '*.terminal',
111+
downloadName: '4bit.terminal',
112+
mimeType: PLIST_MIME_TYPE,
113+
},
103114
{
104115
id: 'mintty',
105116
buttonId: 'mintty-button',
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { bytesToBase64 } from './lib/byte-strings';
2+
import { encodeNsColorArchive } from './lib/ns-color-archive';
3+
4+
const TERMINAL_COLOR_KEYS = [
5+
['black', 'ANSIBlackColor'],
6+
['red', 'ANSIRedColor'],
7+
['green', 'ANSIGreenColor'],
8+
['yellow', 'ANSIYellowColor'],
9+
['blue', 'ANSIBlueColor'],
10+
['magenta', 'ANSIMagentaColor'],
11+
['cyan', 'ANSICyanColor'],
12+
['white', 'ANSIWhiteColor'],
13+
['brightBlack', 'ANSIBrightBlackColor'],
14+
['brightRed', 'ANSIBrightRedColor'],
15+
['brightGreen', 'ANSIBrightGreenColor'],
16+
['brightYellow', 'ANSIBrightYellowColor'],
17+
['brightBlue', 'ANSIBrightBlueColor'],
18+
['brightMagenta', 'ANSIBrightMagentaColor'],
19+
['brightCyan', 'ANSIBrightCyanColor'],
20+
['brightWhite', 'ANSIBrightWhiteColor'],
21+
['background', 'BackgroundColor'],
22+
['foreground', 'TextColor'],
23+
['foreground', 'TextBoldColor'],
24+
['foreground', 'CursorColor'],
25+
['brightBlack', 'SelectionColor'],
26+
];
27+
28+
function dataElement(bytes) {
29+
return `\t<data>${bytesToBase64(bytes)}</data>\n`;
30+
}
31+
32+
function colorDataEntry(colors, colorName, terminalKey) {
33+
let out = '';
34+
35+
out += `\t<key>${terminalKey}</key>\n`;
36+
out += dataElement(encodeNsColorArchive(colors[colorName]));
37+
38+
return out;
39+
}
40+
41+
export function serializeMacosTerminal(colors) {
42+
let out = '';
43+
44+
out += '<?xml version="1.0" encoding="UTF-8"?>\n';
45+
out += '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n';
46+
out += '<plist version="1.0">\n';
47+
out += '<dict>\n';
48+
out += '\t<key>ProfileCurrentVersion</key>\n';
49+
out += '\t<real>2.04</real>\n';
50+
out += '\t<key>name</key>\n';
51+
out += '\t<string>4bit</string>\n';
52+
out += '\t<key>type</key>\n';
53+
out += '\t<string>Window Settings</string>\n';
54+
55+
TERMINAL_COLOR_KEYS.forEach(([colorName, terminalKey]) => {
56+
out += colorDataEntry(colors, colorName, terminalKey);
57+
});
58+
59+
out += '</dict>\n';
60+
out += '</plist>\n';
61+
62+
return out;
63+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { asciiBytes } from './byte-strings';
2+
3+
export class BinaryPlistData {
4+
constructor(bytes) {
5+
this.bytes = bytes;
6+
}
7+
}
8+
9+
export class BinaryPlistUid {
10+
constructor(value) {
11+
this.value = value;
12+
}
13+
}
14+
15+
function concatBytes(parts) {
16+
const length = parts.reduce((sum, part) => sum + part.length, 0);
17+
const bytes = new Uint8Array(length);
18+
let offset = 0;
19+
20+
parts.forEach((part) => {
21+
bytes.set(part, offset);
22+
offset += part.length;
23+
});
24+
25+
return bytes;
26+
}
27+
28+
function minimumByteSize(value) {
29+
if (value <= 0xff) {
30+
return 1;
31+
}
32+
33+
if (value <= 0xffff) {
34+
return 2;
35+
}
36+
37+
if (value <= 0xffffffff) {
38+
return 4;
39+
}
40+
41+
return 8;
42+
}
43+
44+
function integerBytes(value, byteSize = minimumByteSize(value)) {
45+
const bytes = new Uint8Array(byteSize);
46+
let remaining = BigInt(value);
47+
48+
for (let index = byteSize - 1; index >= 0; index -= 1) {
49+
bytes[index] = Number(remaining & 0xffn);
50+
remaining >>= 8n;
51+
}
52+
53+
return bytes;
54+
}
55+
56+
function markerWithLength(marker, length) {
57+
if (length < 15) {
58+
return new Uint8Array([marker | length]);
59+
}
60+
61+
const lengthBytes = integerBytes(length);
62+
const intMarker = 0x10 | Math.log2(lengthBytes.length);
63+
64+
return concatBytes([
65+
new Uint8Array([marker | 0x0f, intMarker]),
66+
lengthBytes,
67+
]);
68+
}
69+
70+
function isScalar(value) {
71+
return value === null ||
72+
typeof value === 'string' ||
73+
typeof value === 'number' ||
74+
value instanceof BinaryPlistData ||
75+
value instanceof BinaryPlistUid;
76+
}
77+
78+
function flattenObject(value, objects) {
79+
const index = objects.length;
80+
objects.push(value);
81+
82+
if (isScalar(value)) {
83+
return index;
84+
}
85+
86+
if (Array.isArray(value)) {
87+
value.forEach((item) => flattenObject(item, objects));
88+
return index;
89+
}
90+
91+
Object.keys(value).forEach((key) => {
92+
flattenObject(key, objects);
93+
flattenObject(value[key], objects);
94+
});
95+
96+
return index;
97+
}
98+
99+
function objectReference(index, objectRefSize) {
100+
return integerBytes(index, objectRefSize);
101+
}
102+
103+
function encodeScalar(value) {
104+
if (value === null) {
105+
return new Uint8Array([0x00]);
106+
}
107+
108+
if (typeof value === 'number') {
109+
const bytes = integerBytes(value);
110+
return concatBytes([new Uint8Array([0x10 | Math.log2(bytes.length)]), bytes]);
111+
}
112+
113+
if (typeof value === 'string') {
114+
const bytes = asciiBytes(value);
115+
return concatBytes([markerWithLength(0x50, bytes.length), bytes]);
116+
}
117+
118+
if (value instanceof BinaryPlistData) {
119+
return concatBytes([markerWithLength(0x40, value.bytes.length), value.bytes]);
120+
}
121+
122+
const bytes = integerBytes(value.value);
123+
return concatBytes([new Uint8Array([0x80 | (bytes.length - 1)]), bytes]);
124+
}
125+
126+
function encodeObject(value, objectIndexes, objectRefSize) {
127+
if (isScalar(value)) {
128+
return encodeScalar(value);
129+
}
130+
131+
if (Array.isArray(value)) {
132+
const references = value.map((item) => objectReference(objectIndexes.get(item), objectRefSize));
133+
134+
return concatBytes([markerWithLength(0xa0, value.length), ...references]);
135+
}
136+
137+
const keys = Object.keys(value);
138+
const keyReferences = keys.map((key) => objectReference(objectIndexes.get(key), objectRefSize));
139+
const valueReferences = keys.map((key) => objectReference(objectIndexes.get(value[key]), objectRefSize));
140+
141+
return concatBytes([markerWithLength(0xd0, keys.length), ...keyReferences, ...valueReferences]);
142+
}
143+
144+
function buildObjectIndexMap(objects) {
145+
const objectIndexes = new Map();
146+
147+
objects.forEach((object, index) => {
148+
objectIndexes.set(object, index);
149+
});
150+
151+
return objectIndexes;
152+
}
153+
154+
function trailer(offsetIntSize, objectRefSize, objectCount, topObjectIndex, offsetTableOffset) {
155+
return concatBytes([
156+
new Uint8Array(6),
157+
new Uint8Array([offsetIntSize, objectRefSize]),
158+
integerBytes(objectCount, 8),
159+
integerBytes(topObjectIndex, 8),
160+
integerBytes(offsetTableOffset, 8),
161+
]);
162+
}
163+
164+
export function encodeBinaryPlist(value) {
165+
const objects = [];
166+
const topObjectIndex = flattenObject(value, objects);
167+
const objectRefSize = minimumByteSize(objects.length - 1);
168+
const objectIndexes = buildObjectIndexMap(objects);
169+
const header = asciiBytes('bplist00');
170+
const offsets = [];
171+
const encodedObjects = [];
172+
let offset = header.length;
173+
174+
objects.forEach((object) => {
175+
const encodedObject = encodeObject(object, objectIndexes, objectRefSize);
176+
177+
offsets.push(offset);
178+
encodedObjects.push(encodedObject);
179+
offset += encodedObject.length;
180+
});
181+
182+
const offsetIntSize = minimumByteSize(offset);
183+
const offsetTableOffset = offset;
184+
const offsetTable = concatBytes(offsets.map((item) => integerBytes(item, offsetIntSize)));
185+
186+
return concatBytes([
187+
header,
188+
...encodedObjects,
189+
offsetTable,
190+
trailer(offsetIntSize, objectRefSize, objects.length, topObjectIndex, offsetTableOffset),
191+
]);
192+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export function asciiBytes(value) {
2+
return new Uint8Array([...value].map((character) => character.charCodeAt(0)));
3+
}
4+
5+
export function bytesToAscii(bytes) {
6+
return String.fromCharCode(...bytes);
7+
}
8+
9+
export function bytesToBase64(bytes) {
10+
let binary = '';
11+
12+
bytes.forEach((byte) => {
13+
binary += String.fromCharCode(byte);
14+
});
15+
16+
return btoa(binary);
17+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { BinaryPlistData, BinaryPlistUid, encodeBinaryPlist } from './binary-plist';
2+
import { asciiBytes } from './byte-strings';
3+
4+
function colorComponent(value) {
5+
if (value === 0) {
6+
return '0';
7+
}
8+
9+
if (value === 255) {
10+
return '1';
11+
}
12+
13+
return Math.fround(value / 255).toFixed(10);
14+
}
15+
16+
export function nsColorRgbString(color) {
17+
const [red, green, blue] = color.rgb().array().map((value) => Math.round(value));
18+
19+
return `${colorComponent(red)} ${colorComponent(green)} ${colorComponent(blue)}\0`;
20+
}
21+
22+
export function encodeNsColorArchive(color) {
23+
return encodeBinaryPlist({
24+
'$version': 100000,
25+
'$objects': [
26+
'$null',
27+
{
28+
NSRGB: new BinaryPlistData(asciiBytes(nsColorRgbString(color))),
29+
NSColorSpace: 2,
30+
'$class': new BinaryPlistUid(2),
31+
},
32+
{
33+
'$classname': 'NSColor',
34+
'$classes': ['NSColor', 'NSObject'],
35+
},
36+
],
37+
'$archiver': 'NSKeyedArchiver',
38+
'$top': {
39+
root: new BinaryPlistUid(1),
40+
},
41+
});
42+
}

src/presentation/about-page/AboutPage.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
<li><a href="https://iterm2.com/">iTerm2</a></li>
9393
<li><a href="https://www.9bis.net/kitty/">KiTTY</a></li>
9494
<li><a href="https://konsole.kde.org/">Konsole</a></li>
95+
<li><a href="https://support.apple.com/guide/terminal/welcome/mac">macOS Terminal.app</a></li>
9596
<li><a href="https://apps.kde.org/yakuake/">Yakuake</a></li>
9697
<li><a href="https://mintty.github.io/">mintty</a></li>
9798
<li><a href="https://www.chiark.greenend.org.uk/~sgtatham/putty/">PuTTY</a></li>

0 commit comments

Comments
 (0)