fpasoterm 1.6.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.
Potentially problematic release.
This version of fpasoterm might be problematic. Click here for more details.
- package/CHANGELOG.md +230 -0
- package/CONTRIBUTING.md +67 -0
- package/INSTALL.ja.md +204 -0
- package/INSTALL.md +229 -0
- package/LICENSE +21 -0
- package/README.ja.md +389 -0
- package/README.md +607 -0
- package/bin/fpasoterm +2882 -0
- package/completions/_fpasoterm +81 -0
- package/completions/fpasoterm.bash +55 -0
- package/completions/fpasoterm.fish +64 -0
- package/completions/fpasoterm.ps1 +47 -0
- package/docs/capability-diagnostics.en.md +77 -0
- package/docs/capability-diagnostics.ja.md +63 -0
- package/docs/completion.en.md +141 -0
- package/docs/completion.ja.md +113 -0
- package/docs/config.en.md +554 -0
- package/docs/config.ja.md +544 -0
- package/docs/debugging.en.md +72 -0
- package/docs/debugging.ja.md +66 -0
- package/docs/diagnostics.en.md +69 -0
- package/docs/diagnostics.ja.md +64 -0
- package/docs/font-diagnostics.en.md +116 -0
- package/docs/font-diagnostics.ja.md +105 -0
- package/docs/fpasoterm-plugin.d.ts +51 -0
- package/docs/known-issues.en.md +23 -0
- package/docs/known-issues.ja.md +23 -0
- package/docs/plugins.en.md +266 -0
- package/docs/plugins.ja.md +176 -0
- package/docs/pr-review.en.md +177 -0
- package/docs/pr-review.ja.md +170 -0
- package/docs/release-checklist.en.md +72 -0
- package/docs/release-checklist.ja.md +71 -0
- package/docs/security.en.md +36 -0
- package/docs/security.ja.md +31 -0
- package/docs/spec.en.md +106 -0
- package/docs/spec.ja.md +99 -0
- package/docs/sshfs.en.md +28 -0
- package/docs/sshfs.ja.md +28 -0
- package/docs/sync.en.md +280 -0
- package/docs/sync.ja.md +280 -0
- package/examples/apply-default-appearance.bat +8 -0
- package/examples/apply-default-appearance.ps1 +7 -0
- package/examples/apply-default-appearance.sh +7 -0
- package/examples/apply-runtime-appearance.bat +8 -0
- package/examples/apply-runtime-appearance.ps1 +7 -0
- package/examples/apply-runtime-appearance.sh +7 -0
- package/examples/config/default-appearance.toml +36 -0
- package/examples/config/minimal.toml +30 -0
- package/examples/config/profiles.toml +20 -0
- package/examples/config/runtime-appearance.toml +24 -0
- package/examples/config/sync-folder.toml +17 -0
- package/examples/config/tui-compatibility.toml +7 -0
- package/examples/config/with-plugins.toml +33 -0
- package/examples/plugins/hello.ts +17 -0
- package/examples/plugins/status-banner.ts +14 -0
- package/examples/plugins/theme.ts +32 -0
- package/examples/plugins/welcome-banner.ts +16 -0
- package/extra/linux/icons/hicolor/128x128/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/16x16/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/192x192/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/256x256/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/32x32/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/48x48/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/512x512/apps/fpasoterm.png +0 -0
- package/extra/linux/icons/hicolor/64x64/apps/fpasoterm.png +0 -0
- package/extra/linux/io.github.oyoguhito.fpasoterm.desktop +10 -0
- package/extra/logo/fpasoterm.png +0 -0
- package/extra/macos/fpasoterm.icns +0 -0
- package/extra/windows/fpasoterm.cmd +40 -0
- package/extra/windows/fpasoterm.ico +0 -0
- package/package.json +81 -0
- package/scripts/build-artifacts.js +183 -0
- package/scripts/generate-icon.js +251 -0
- package/scripts/init-jj-empty-main +41 -0
- package/scripts/install-linux-desktop.js +238 -0
- package/scripts/run +16 -0
- package/scripts/security/scan-secrets.js +89 -0
- package/scripts/tests/smoke.js +2476 -0
- package/scripts/uninstall-desktop.js +20 -0
- package/scripts/uninstall-linux-desktop.js +96 -0
- package/scripts/uninstall-windows-path.js +94 -0
- package/src/config.js +855 -0
- package/src/renderer/confirm.html +71 -0
- package/src/renderer/index.html +223 -0
- package/src/renderer/renderer.js +4166 -0
- package/src/renderer/styles.css +969 -0
- package/src/renderer/vendor/addon-fit/addon-fit.js +2 -0
- package/src/renderer/vendor/addon-image/LICENSE +19 -0
- package/src/renderer/vendor/addon-image/addon-image.js +3 -0
- package/src/renderer/vendor/addon-web-links/LICENSE +19 -0
- package/src/renderer/vendor/addon-web-links/addon-web-links.js +2 -0
- package/src/renderer/vendor/xterm/xterm.css +292 -0
- package/src/renderer/vendor/xterm/xterm.js +2 -0
- package/src-tauri/Cargo.lock +5253 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +140 -0
- package/src-tauri/capabilities/default.json +13 -0
- package/src-tauri/default-config.toml +128 -0
- package/src-tauri/src/main.rs +9347 -0
- package/src-tauri/tauri.conf.json +60 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const zlib = require('node:zlib');
|
|
4
|
+
|
|
5
|
+
const root = path.resolve(__dirname, '..');
|
|
6
|
+
const sourceSize = 256;
|
|
7
|
+
const pixels = Buffer.alloc(sourceSize * sourceSize * 4);
|
|
8
|
+
|
|
9
|
+
// Writes one RGBA pixel into the source icon buffer.
|
|
10
|
+
function setPixel(x, y, r, g, b, a = 255) {
|
|
11
|
+
const offset = (y * sourceSize + x) * 4;
|
|
12
|
+
pixels[offset] = r;
|
|
13
|
+
pixels[offset + 1] = g;
|
|
14
|
+
pixels[offset + 2] = b;
|
|
15
|
+
pixels[offset + 3] = a;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Fills an axis-aligned rectangle in the source icon buffer.
|
|
19
|
+
function fillRect(x, y, width, height, color) {
|
|
20
|
+
for (let row = y; row < y + height; row += 1) {
|
|
21
|
+
for (let col = x; col < x + width; col += 1) {
|
|
22
|
+
setPixel(col, row, ...color);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fills a rectangle with rounded corners for the terminal-window shape.
|
|
28
|
+
function fillRoundedRect(x, y, width, height, radius, color) {
|
|
29
|
+
for (let row = y; row < y + height; row += 1) {
|
|
30
|
+
for (let col = x; col < x + width; col += 1) {
|
|
31
|
+
const dx = Math.max(x - col + radius, 0, col - (x + width - radius - 1));
|
|
32
|
+
const dy = Math.max(y - row + radius, 0, row - (y + height - radius - 1));
|
|
33
|
+
if ((dx * dx) + (dy * dy) <= radius * radius) {
|
|
34
|
+
setPixel(col, row, ...color);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Draws a rectangular outline by filling four thin rectangles.
|
|
41
|
+
function strokeRect(x, y, width, height, thickness, color) {
|
|
42
|
+
fillRect(x, y, width, thickness, color);
|
|
43
|
+
fillRect(x, y + height - thickness, width, thickness, color);
|
|
44
|
+
fillRect(x, y, thickness, height, color);
|
|
45
|
+
fillRect(x + width - thickness, y, thickness, height, color);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Draws a thick line using small rounded rectangles along a Bresenham path.
|
|
49
|
+
function line(x0, y0, x1, y1, thickness, color) {
|
|
50
|
+
const dx = Math.abs(x1 - x0);
|
|
51
|
+
const dy = Math.abs(y1 - y0);
|
|
52
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
53
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
54
|
+
let err = dx - dy;
|
|
55
|
+
let x = x0;
|
|
56
|
+
let y = y0;
|
|
57
|
+
|
|
58
|
+
while (true) {
|
|
59
|
+
fillRoundedRect(x - thickness, y - thickness, thickness * 2, thickness * 2, thickness, color);
|
|
60
|
+
if (x === x1 && y === y1) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const e2 = err * 2;
|
|
64
|
+
if (e2 > -dy) {
|
|
65
|
+
err -= dy;
|
|
66
|
+
x += sx;
|
|
67
|
+
}
|
|
68
|
+
if (e2 < dx) {
|
|
69
|
+
err += dx;
|
|
70
|
+
y += sy;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Computes PNG chunk checksums.
|
|
76
|
+
function crc32(buffer) {
|
|
77
|
+
let crc = 0xffffffff;
|
|
78
|
+
for (const byte of buffer) {
|
|
79
|
+
crc ^= byte;
|
|
80
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
81
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Creates a single PNG chunk with length, type, data, and CRC.
|
|
88
|
+
function chunk(type, data) {
|
|
89
|
+
const typeBuffer = Buffer.from(type);
|
|
90
|
+
const length = Buffer.alloc(4);
|
|
91
|
+
length.writeUInt32BE(data.length);
|
|
92
|
+
const crc = Buffer.alloc(4);
|
|
93
|
+
crc.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])));
|
|
94
|
+
return Buffer.concat([length, typeBuffer, data, crc]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Encodes raw RGBA pixels into a minimal PNG file.
|
|
98
|
+
function encodePng(imageSize, imagePixels) {
|
|
99
|
+
const raw = Buffer.alloc((imageSize * 4 + 1) * imageSize);
|
|
100
|
+
for (let y = 0; y < imageSize; y += 1) {
|
|
101
|
+
const rowStart = y * (imageSize * 4 + 1);
|
|
102
|
+
raw[rowStart] = 0;
|
|
103
|
+
imagePixels.copy(raw, rowStart + 1, y * imageSize * 4, (y + 1) * imageSize * 4);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const ihdr = Buffer.alloc(13);
|
|
107
|
+
ihdr.writeUInt32BE(imageSize, 0);
|
|
108
|
+
ihdr.writeUInt32BE(imageSize, 4);
|
|
109
|
+
ihdr[8] = 8;
|
|
110
|
+
ihdr[9] = 6;
|
|
111
|
+
ihdr[10] = 0;
|
|
112
|
+
ihdr[11] = 0;
|
|
113
|
+
ihdr[12] = 0;
|
|
114
|
+
|
|
115
|
+
return Buffer.concat([
|
|
116
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
117
|
+
chunk('IHDR', ihdr),
|
|
118
|
+
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
|
119
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Encodes one macOS icon chunk from a PNG payload.
|
|
124
|
+
function iconChunk(type, pngData) {
|
|
125
|
+
const chunkHeader = Buffer.alloc(8);
|
|
126
|
+
chunkHeader.write(type, 0, 4, 'ascii');
|
|
127
|
+
chunkHeader.writeUInt32BE(pngData.length + 8, 4);
|
|
128
|
+
return Buffer.concat([chunkHeader, pngData]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Encodes a macOS .icns file with PNG-backed icon representations.
|
|
132
|
+
function encodeIcns(iconEntries) {
|
|
133
|
+
const body = Buffer.concat(iconEntries.map(({ type, pngData }) => iconChunk(type, pngData)));
|
|
134
|
+
const header = Buffer.alloc(8);
|
|
135
|
+
header.write('icns', 0, 4, 'ascii');
|
|
136
|
+
header.writeUInt32BE(body.length + 8, 4);
|
|
137
|
+
return Buffer.concat([header, body]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Writes a Windows .ico file that embeds PNG icon images.
|
|
141
|
+
function encodeIco(iconEntries) {
|
|
142
|
+
const imageCount = iconEntries.length;
|
|
143
|
+
const header = Buffer.alloc(6);
|
|
144
|
+
header.writeUInt16LE(0, 0);
|
|
145
|
+
header.writeUInt16LE(1, 2);
|
|
146
|
+
header.writeUInt16LE(imageCount, 4);
|
|
147
|
+
|
|
148
|
+
const directory = Buffer.alloc(imageCount * 16);
|
|
149
|
+
let offset = 6 + imageCount * 16;
|
|
150
|
+
const images = [];
|
|
151
|
+
|
|
152
|
+
iconEntries.forEach(({ size, pngData }, index) => {
|
|
153
|
+
const entryOffset = index * 16;
|
|
154
|
+
directory[entryOffset] = size >= 256 ? 0 : size;
|
|
155
|
+
directory[entryOffset + 1] = size >= 256 ? 0 : size;
|
|
156
|
+
directory[entryOffset + 2] = 0;
|
|
157
|
+
directory[entryOffset + 3] = 0;
|
|
158
|
+
directory.writeUInt16LE(1, entryOffset + 4);
|
|
159
|
+
directory.writeUInt16LE(32, entryOffset + 6);
|
|
160
|
+
directory.writeUInt32LE(pngData.length, entryOffset + 8);
|
|
161
|
+
directory.writeUInt32LE(offset, entryOffset + 12);
|
|
162
|
+
offset += pngData.length;
|
|
163
|
+
images.push(pngData);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return Buffer.concat([header, directory, ...images]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Resizes the source icon using nearest-neighbor sampling for crisp small icons.
|
|
170
|
+
function resizePixels(targetSize) {
|
|
171
|
+
if (targetSize === sourceSize) {
|
|
172
|
+
return Buffer.from(pixels);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const resized = Buffer.alloc(targetSize * targetSize * 4);
|
|
176
|
+
for (let y = 0; y < targetSize; y += 1) {
|
|
177
|
+
for (let x = 0; x < targetSize; x += 1) {
|
|
178
|
+
const sourceX = Math.min(sourceSize - 1, Math.floor((x * sourceSize) / targetSize));
|
|
179
|
+
const sourceY = Math.min(sourceSize - 1, Math.floor((y * sourceSize) / targetSize));
|
|
180
|
+
const sourceOffset = (sourceY * sourceSize + sourceX) * 4;
|
|
181
|
+
const targetOffset = (y * targetSize + x) * 4;
|
|
182
|
+
pixels.copy(resized, targetOffset, sourceOffset, sourceOffset + 4);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return resized;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Writes one icon file at the requested size.
|
|
189
|
+
function writeIcon(relativePath, imageSize) {
|
|
190
|
+
const outputPath = path.join(root, relativePath);
|
|
191
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
192
|
+
fs.writeFileSync(outputPath, encodePng(imageSize, resizePixels(imageSize)));
|
|
193
|
+
console.log(outputPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (let y = 0; y < sourceSize; y += 1) {
|
|
197
|
+
for (let x = 0; x < sourceSize; x += 1) {
|
|
198
|
+
const shade = Math.round(18 + (x + y) * 0.08);
|
|
199
|
+
setPixel(x, y, shade, shade + 8, shade + 18);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fillRoundedRect(32, 48, 192, 152, 18, [10, 16, 24, 255]);
|
|
204
|
+
strokeRect(32, 48, 192, 152, 7, [61, 75, 95, 255]);
|
|
205
|
+
fillRoundedRect(48, 64, 160, 24, 6, [26, 34, 48, 255]);
|
|
206
|
+
fillRoundedRect(61, 71, 10, 10, 5, [255, 107, 107, 255]);
|
|
207
|
+
fillRoundedRect(81, 71, 10, 10, 5, [245, 215, 110, 255]);
|
|
208
|
+
fillRoundedRect(101, 71, 10, 10, 5, [139, 209, 124, 255]);
|
|
209
|
+
line(67, 119, 94, 140, 5, [143, 211, 255, 255]);
|
|
210
|
+
line(94, 140, 67, 161, 5, [143, 211, 255, 255]);
|
|
211
|
+
fillRoundedRect(108, 156, 56, 14, 7, [232, 237, 242, 255]);
|
|
212
|
+
fillRoundedRect(145, 105, 60, 48, 12, [245, 215, 110, 255]);
|
|
213
|
+
fillRoundedRect(158, 118, 34, 10, 5, [16, 21, 29, 255]);
|
|
214
|
+
fillRoundedRect(158, 135, 34, 10, 5, [16, 21, 29, 255]);
|
|
215
|
+
fillRoundedRect(64, 216, 128, 8, 4, [245, 215, 110, 255]);
|
|
216
|
+
|
|
217
|
+
writeIcon('extra/logo/fpasoterm.png', 256);
|
|
218
|
+
|
|
219
|
+
for (const iconSize of [16, 32, 48, 64, 128, 192, 256, 512]) {
|
|
220
|
+
writeIcon(`extra/linux/icons/hicolor/${iconSize}x${iconSize}/apps/fpasoterm.png`, iconSize);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const macIconEntries = [
|
|
224
|
+
{ type: 'icp4', size: 16 },
|
|
225
|
+
{ type: 'icp5', size: 32 },
|
|
226
|
+
{ type: 'icp6', size: 64 },
|
|
227
|
+
{ type: 'ic07', size: 128 },
|
|
228
|
+
{ type: 'ic08', size: 256 },
|
|
229
|
+
{ type: 'ic09', size: 512 },
|
|
230
|
+
{ type: 'ic10', size: 1024 },
|
|
231
|
+
].map(({ type, size }) => ({
|
|
232
|
+
type,
|
|
233
|
+
pngData: encodePng(size, resizePixels(size)),
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
const macIcnsPath = path.join(root, 'extra', 'macos', 'fpasoterm.icns');
|
|
237
|
+
fs.mkdirSync(path.dirname(macIcnsPath), { recursive: true });
|
|
238
|
+
fs.writeFileSync(macIcnsPath, encodeIcns(macIconEntries));
|
|
239
|
+
console.log(macIcnsPath);
|
|
240
|
+
|
|
241
|
+
const windowsIconEntries = [
|
|
242
|
+
16, 32, 48, 64, 128, 256,
|
|
243
|
+
].map((size) => ({
|
|
244
|
+
size,
|
|
245
|
+
pngData: encodePng(size, resizePixels(size)),
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
const windowsIcoPath = path.join(root, 'extra', 'windows', 'fpasoterm.ico');
|
|
249
|
+
fs.mkdirSync(path.dirname(windowsIcoPath), { recursive: true });
|
|
250
|
+
fs.writeFileSync(windowsIcoPath, encodeIco(windowsIconEntries));
|
|
251
|
+
console.log(windowsIcoPath);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
if ! command -v jj >/dev/null 2>&1; then
|
|
5
|
+
echo "jj is not installed. Install jj first, then rerun this script." >&2
|
|
6
|
+
exit 127
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
if [ -d .jj ] || [ -d .git ]; then
|
|
10
|
+
echo "This directory already looks initialized. Refusing to overwrite repository metadata." >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
tmpdir=$(mktemp -d)
|
|
15
|
+
restore_needed=1
|
|
16
|
+
cleanup() {
|
|
17
|
+
status=$?
|
|
18
|
+
if [ "$restore_needed" = 1 ] && [ -d "$tmpdir" ]; then
|
|
19
|
+
find "$tmpdir" -mindepth 1 -maxdepth 1 -exec mv {} . \;
|
|
20
|
+
fi
|
|
21
|
+
if [ "$status" = 0 ]; then
|
|
22
|
+
rm -rf "$tmpdir"
|
|
23
|
+
else
|
|
24
|
+
echo "Initialization failed. Project files were restored; temporary directory left at: $tmpdir" >&2
|
|
25
|
+
fi
|
|
26
|
+
exit "$status"
|
|
27
|
+
}
|
|
28
|
+
trap cleanup EXIT INT TERM
|
|
29
|
+
|
|
30
|
+
find . -mindepth 1 -maxdepth 1 ! -name .jj ! -name .git -exec mv {} "$tmpdir"/ \;
|
|
31
|
+
|
|
32
|
+
jj git init --colocate .
|
|
33
|
+
jj describe -m "empty main"
|
|
34
|
+
jj bookmark create main -r @
|
|
35
|
+
|
|
36
|
+
jj new main -m "Initial fpasoterm terminal app"
|
|
37
|
+
find "$tmpdir" -mindepth 1 -maxdepth 1 -exec mv {} . \;
|
|
38
|
+
restore_needed=0
|
|
39
|
+
|
|
40
|
+
jj file track .
|
|
41
|
+
jj status
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const childProcess = require('node:child_process');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
// This script installs a Linux desktop entry and icon theme assets. Windows
|
|
7
|
+
// packages own their shortcuts, so do not create an unusable Unix wrapper.
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
console.log('update:desktop is only needed for Linux/ChromeOS desktop integration; use the Windows MSI or EXE instead.');
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(__dirname, '..');
|
|
14
|
+
const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
15
|
+
const binHome = process.env.XDG_BIN_HOME || path.join(os.homedir(), '.local', 'bin');
|
|
16
|
+
const applicationsDir = path.join(dataHome, 'applications');
|
|
17
|
+
const iconsDir = path.join(dataHome, 'icons', 'hicolor');
|
|
18
|
+
const iconSizes = [16, 32, 48, 64, 128, 192, 256, 512];
|
|
19
|
+
const buildStampPath = path.join(root, 'src-tauri', 'target', 'debug', '.fpasoterm-normal-build.json');
|
|
20
|
+
|
|
21
|
+
// Builds a current local Tauri binary so launcher icon starts do not fall back
|
|
22
|
+
// to `tauri dev`, which runs Cargo watch and looks like a failed GUI launch.
|
|
23
|
+
function buildLocalBinary() {
|
|
24
|
+
if (process.env.FPASOTERM_SKIP_DESKTOP_BUILD === '1') {
|
|
25
|
+
console.log('skipping local Tauri build because FPASOTERM_SKIP_DESKTOP_BUILD=1');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Tauri's frontend assets are embedded at compile time. Cleaning only this
|
|
30
|
+
// crate makes update:desktop rebuild renderer-only changes as well.
|
|
31
|
+
const cleanResult = childProcess.spawnSync(
|
|
32
|
+
'cargo',
|
|
33
|
+
['clean', '--manifest-path', path.join(root, 'src-tauri', 'Cargo.toml'), '-p', 'fpasoterm'],
|
|
34
|
+
{ stdio: 'inherit' },
|
|
35
|
+
);
|
|
36
|
+
if (cleanResult.error && cleanResult.error.code === 'ENOENT') {
|
|
37
|
+
console.warn('cargo is not available; launcher may fall back to an older local binary');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (cleanResult.error) {
|
|
41
|
+
throw cleanResult.error;
|
|
42
|
+
}
|
|
43
|
+
if (cleanResult.status !== 0) {
|
|
44
|
+
throw new Error(`cargo clean failed with exit code ${cleanResult.status}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = childProcess.spawnSync(
|
|
48
|
+
'cargo',
|
|
49
|
+
['build', '--manifest-path', path.join(root, 'src-tauri', 'Cargo.toml')],
|
|
50
|
+
{ stdio: 'inherit' },
|
|
51
|
+
);
|
|
52
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
53
|
+
console.warn('cargo is not available; launcher may fall back to Tauri dev until a binary is built');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (result.error) {
|
|
57
|
+
throw result.error;
|
|
58
|
+
}
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
throw new Error(`cargo build failed with exit code ${result.status}`);
|
|
61
|
+
}
|
|
62
|
+
writeBuildStamp(path.join(root, 'src-tauri', 'target', 'debug', process.platform === 'win32' ? 'fpasoterm.exe' : 'fpasoterm'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Tracks source files that require rebuilding the local Tauri binary.
|
|
66
|
+
function latestRuntimeSourceMtime() {
|
|
67
|
+
return [
|
|
68
|
+
'src-tauri/src/main.rs',
|
|
69
|
+
'src-tauri/Cargo.toml',
|
|
70
|
+
'src-tauri/Cargo.lock',
|
|
71
|
+
'src-tauri/build.rs',
|
|
72
|
+
'src-tauri/default-config.toml',
|
|
73
|
+
'src-tauri/tauri.conf.json',
|
|
74
|
+
'src-tauri/capabilities',
|
|
75
|
+
'src/renderer',
|
|
76
|
+
'extra/logo/fpasoterm.png',
|
|
77
|
+
'package.json',
|
|
78
|
+
]
|
|
79
|
+
.map((relativePath) => path.join(root, relativePath))
|
|
80
|
+
.reduce((latest, sourcePath) => Math.max(latest, latestPathMtime(sourcePath)), 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Returns the newest mtime in a file or source directory recursively.
|
|
84
|
+
function latestPathMtime(sourcePath) {
|
|
85
|
+
try {
|
|
86
|
+
const stat = fs.statSync(sourcePath);
|
|
87
|
+
if (!stat.isDirectory()) {
|
|
88
|
+
return stat.mtimeMs;
|
|
89
|
+
}
|
|
90
|
+
return fs.readdirSync(sourcePath, { withFileTypes: true })
|
|
91
|
+
.reduce((latest, entry) => Math.max(latest, latestPathMtime(path.join(sourcePath, entry.name))), stat.mtimeMs);
|
|
92
|
+
} catch {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Writes the same normal-build stamp used by bin/fpasoterm to skip rebuilds.
|
|
98
|
+
function writeBuildStamp(binary) {
|
|
99
|
+
if (!fs.existsSync(binary)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
fs.writeFileSync(buildStampPath, JSON.stringify({
|
|
103
|
+
binary,
|
|
104
|
+
binaryMtime: fs.statSync(binary).mtimeMs,
|
|
105
|
+
sourceMtime: latestRuntimeSourceMtime(),
|
|
106
|
+
}, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Copies one desktop or icon file, creating the target directory first.
|
|
110
|
+
function copyFile(source, target) {
|
|
111
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
112
|
+
fs.copyFileSync(source, target);
|
|
113
|
+
console.log(`${source} -> ${target}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Runs optional desktop cache refresh commands when they exist on the host.
|
|
117
|
+
function runOptional(command, args) {
|
|
118
|
+
const result = childProcess.spawnSync(command, args, { stdio: 'inherit' });
|
|
119
|
+
if (result.error && result.error.code !== 'ENOENT') {
|
|
120
|
+
throw result.error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Removes legacy files created by older fpasoterm versions.
|
|
125
|
+
function removeFile(target) {
|
|
126
|
+
if (fs.existsSync(target)) {
|
|
127
|
+
fs.rmSync(target, { force: true });
|
|
128
|
+
console.log(`removed old file: ${target}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Quotes paths safely for the generated shell wrapper.
|
|
133
|
+
function shellQuote(value) {
|
|
134
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Returns the Node.js executable used to run this installer when it is stable.
|
|
138
|
+
function currentNodePath() {
|
|
139
|
+
return path.isAbsolute(process.execPath) && fs.existsSync(process.execPath) ? process.execPath : '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Formats the Exec command path for a desktop entry. ChromeOS Linux launchers
|
|
143
|
+
// are more reliable with a plain absolute path when the wrapper path has no
|
|
144
|
+
// spaces.
|
|
145
|
+
function desktopExec(value) {
|
|
146
|
+
if (/[\s"'\\]/.test(value)) {
|
|
147
|
+
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Installs a desktop entry that points at the absolute wrapper path.
|
|
153
|
+
function installDesktopEntry(commandPath) {
|
|
154
|
+
const source = path.join(root, 'extra', 'linux', 'io.github.oyoguhito.fpasoterm.desktop');
|
|
155
|
+
const target = path.join(applicationsDir, 'io.github.oyoguhito.fpasoterm.desktop');
|
|
156
|
+
const shortTarget = path.join(applicationsDir, 'fpasoterm.desktop');
|
|
157
|
+
const desktop = fs.readFileSync(source, 'utf8')
|
|
158
|
+
.replace(/^Exec=.*$/m, `Exec=${desktopExec(commandPath)}`)
|
|
159
|
+
.replace(/^Icon=.*$/m, 'Icon=io.github.oyoguhito.fpasoterm');
|
|
160
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
161
|
+
fs.writeFileSync(target, desktop);
|
|
162
|
+
fs.writeFileSync(shortTarget, desktop);
|
|
163
|
+
console.log(`${source} -> ${target}`);
|
|
164
|
+
console.log(`${source} -> ${shortTarget}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Installs the local fpasoterm command wrapper into the user's bin directory.
|
|
168
|
+
function installCommand() {
|
|
169
|
+
fs.mkdirSync(binHome, { recursive: true });
|
|
170
|
+
const commandPath = path.join(binHome, 'fpasoterm');
|
|
171
|
+
const nodePath = currentNodePath();
|
|
172
|
+
const script = `#!/bin/sh
|
|
173
|
+
set -eu
|
|
174
|
+
|
|
175
|
+
APP_ROOT=${shellQuote(root)}
|
|
176
|
+
INSTALL_NODE=${nodePath ? shellQuote(nodePath) : "''"}
|
|
177
|
+
LOG_DIR="\${XDG_CACHE_HOME:-$HOME/.cache}/fpasoterm"
|
|
178
|
+
LOG_FILE="$LOG_DIR/launcher.log"
|
|
179
|
+
PATH="$HOME/.cargo/bin:$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"
|
|
180
|
+
FPASOTERM_LAUNCHER_LOG="$LOG_FILE"
|
|
181
|
+
export PATH FPASOTERM_LAUNCHER_LOG
|
|
182
|
+
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
|
183
|
+
{
|
|
184
|
+
printf '%s\\n' "--- fpasoterm launcher $(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true) ---"
|
|
185
|
+
printf 'argv=%s\\n' "$*"
|
|
186
|
+
printf 'pwd=%s\\n' "$(pwd 2>/dev/null || true)"
|
|
187
|
+
printf 'PATH=%s\\n' "\${PATH:-}"
|
|
188
|
+
printf 'DISPLAY=%s\\n' "\${DISPLAY:-}"
|
|
189
|
+
printf 'WAYLAND_DISPLAY=%s\\n' "\${WAYLAND_DISPLAY:-}"
|
|
190
|
+
printf 'XDG_RUNTIME_DIR=%s\\n' "\${XDG_RUNTIME_DIR:-}"
|
|
191
|
+
printf 'APP_ROOT=%s\\n' "$APP_ROOT"
|
|
192
|
+
printf 'INSTALL_NODE=%s\\n' "$INSTALL_NODE"
|
|
193
|
+
} >>"$LOG_FILE" 2>/dev/null || true
|
|
194
|
+
cd "$APP_ROOT"
|
|
195
|
+
|
|
196
|
+
if [ -n "$INSTALL_NODE" ] && [ -x "$INSTALL_NODE" ]; then
|
|
197
|
+
printf 'exec=%s\\n' "$INSTALL_NODE" >>"$LOG_FILE" 2>/dev/null || true
|
|
198
|
+
exec "$INSTALL_NODE" "$APP_ROOT/bin/fpasoterm" "$@"
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
if command -v node >/dev/null 2>&1; then
|
|
202
|
+
printf 'exec=%s\\n' "$(command -v node)" >>"$LOG_FILE" 2>/dev/null || true
|
|
203
|
+
exec node "$APP_ROOT/bin/fpasoterm" "$@"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
for candidate in /usr/bin/node /usr/local/bin/node "$HOME/.local/bin/node"; do
|
|
207
|
+
if [ -x "$candidate" ]; then
|
|
208
|
+
printf 'exec=%s\\n' "$candidate" >>"$LOG_FILE" 2>/dev/null || true
|
|
209
|
+
exec "$candidate" "$APP_ROOT/bin/fpasoterm" "$@"
|
|
210
|
+
fi
|
|
211
|
+
done
|
|
212
|
+
|
|
213
|
+
echo "node is not available. Install Node.js and run npm run update:desktop again." >&2
|
|
214
|
+
printf 'error=node is not available\\n' >>"$LOG_FILE" 2>/dev/null || true
|
|
215
|
+
exit 127
|
|
216
|
+
`;
|
|
217
|
+
fs.writeFileSync(commandPath, script, { mode: 0o755 });
|
|
218
|
+
fs.chmodSync(commandPath, 0o755);
|
|
219
|
+
console.log(`fpasoterm command -> ${commandPath}`);
|
|
220
|
+
return commandPath;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
buildLocalBinary();
|
|
224
|
+
|
|
225
|
+
const commandPath = installCommand();
|
|
226
|
+
|
|
227
|
+
installDesktopEntry(commandPath);
|
|
228
|
+
removeFile(path.join(applicationsDir, 'io.github.oyoguhito.FpasoTerm.desktop'));
|
|
229
|
+
|
|
230
|
+
for (const size of iconSizes) {
|
|
231
|
+
const sourceIcon = path.join(root, 'extra', 'linux', 'icons', 'hicolor', `${size}x${size}`, 'apps', 'fpasoterm.png');
|
|
232
|
+
const targetIconDir = path.join(iconsDir, `${size}x${size}`, 'apps');
|
|
233
|
+
copyFile(sourceIcon, path.join(targetIconDir, 'fpasoterm.png'));
|
|
234
|
+
copyFile(sourceIcon, path.join(targetIconDir, 'io.github.oyoguhito.fpasoterm.png'));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
runOptional('update-desktop-database', [applicationsDir]);
|
|
238
|
+
runOptional('gtk-update-icon-cache', ['-q', iconsDir]);
|
package/scripts/run
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
5
|
+
cd "$SCRIPT_DIR"
|
|
6
|
+
|
|
7
|
+
if command -v node >/dev/null 2>&1; then
|
|
8
|
+
exec node ./bin/fpasoterm "$@"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
if command -v mise >/dev/null 2>&1; then
|
|
12
|
+
exec mise exec node -- node ./bin/fpasoterm "$@"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
echo "node is not available. Install Node.js or enable mise's Node toolchain." >&2
|
|
16
|
+
exit 127
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const root = path.resolve(__dirname, '..', '..');
|
|
5
|
+
const ignoredDirs = new Set([
|
|
6
|
+
'.git',
|
|
7
|
+
'.jj',
|
|
8
|
+
'artifacts',
|
|
9
|
+
'node_modules',
|
|
10
|
+
'diagnostics',
|
|
11
|
+
'target',
|
|
12
|
+
'gen',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const ignoredFiles = new Set([
|
|
16
|
+
'package-lock.json',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const patterns = [
|
|
20
|
+
['AWS access key', /AKIA[0-9A-Z]{16}/],
|
|
21
|
+
['GitHub token', /gh[pousr]_[A-Za-z0-9_]{36,}/],
|
|
22
|
+
['npm token', /npm_[A-Za-z0-9]{36,}/],
|
|
23
|
+
['Slack token', /xox[baprs]-[A-Za-z0-9-]{10,}/],
|
|
24
|
+
['Google API key', /AIza[0-9A-Za-z_-]{35}/],
|
|
25
|
+
['Private key header', /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/],
|
|
26
|
+
['Generic assigned secret', /\b(?:api[_-]?key|secret|token|password|passwd|pwd)\b\s*[:=]\s*['"][^'"]{12,}['"]/i],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Recursively returns repository files that should be scanned.
|
|
30
|
+
function walk(dir) {
|
|
31
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
32
|
+
const files = [];
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (ignoredDirs.has(entry.name)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fullPath = path.join(dir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
files.push(...walk(fullPath));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (entry.isFile()) {
|
|
46
|
+
files.push(fullPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Heuristically skips binary files to avoid noisy text scanning.
|
|
54
|
+
function isBinary(buffer) {
|
|
55
|
+
return buffer.includes(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const findings = [];
|
|
59
|
+
|
|
60
|
+
for (const file of walk(root)) {
|
|
61
|
+
const relative = path.relative(root, file);
|
|
62
|
+
if (ignoredFiles.has(relative)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const buffer = fs.readFileSync(file);
|
|
67
|
+
if (isBinary(buffer)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = buffer.toString('utf8').split(/\r?\n/);
|
|
72
|
+
for (const [index, line] of lines.entries()) {
|
|
73
|
+
for (const [name, pattern] of patterns) {
|
|
74
|
+
if (pattern.test(line)) {
|
|
75
|
+
findings.push(`${relative}:${index + 1}: possible ${name}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (findings.length > 0) {
|
|
82
|
+
console.error('Potential credentials found:');
|
|
83
|
+
for (const finding of findings) {
|
|
84
|
+
console.error(`- ${finding}`);
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('secret scan passed');
|