Skip to content

Commit 9db3908

Browse files
committed
chore: sync with upstream pingdotgg/t3code main
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2 parents dcf444f + ebe8afb commit 9db3908

278 files changed

Lines changed: 12557 additions & 1094 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Mobile Showcase Screenshots
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
platform:
7+
description: Device platforms to capture
8+
required: true
9+
default: all
10+
type: choice
11+
options:
12+
- all
13+
- ios
14+
- android
15+
appearance:
16+
description: System appearances to capture
17+
required: true
18+
default: both
19+
type: choice
20+
options:
21+
- both
22+
- dark
23+
- light
24+
25+
permissions:
26+
contents: read
27+
28+
env:
29+
NODE_OPTIONS: --max-old-space-size=8192
30+
31+
jobs:
32+
ios:
33+
name: iPhone 6.9, iPhone 6.5, and iPad 13
34+
if: inputs.platform == 'all' || inputs.platform == 'ios'
35+
runs-on: blacksmith-12vcpu-macos-26
36+
timeout-minutes: 60
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v6
40+
41+
- name: Setup Vite+
42+
uses: voidzero-dev/setup-vp@v1
43+
with:
44+
node-version-file: package.json
45+
cache: true
46+
run-install: true
47+
48+
- name: Expose pnpm
49+
run: |
50+
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
51+
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
52+
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
53+
"$vp_pnpm_bin/pnpm" --version
54+
55+
- name: Capture iOS showcase
56+
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}"
57+
58+
- name: Validate App Store Connect assets
59+
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --validate-only
60+
61+
- name: Upload iOS screenshots
62+
if: always()
63+
uses: actions/upload-artifact@v7
64+
with:
65+
name: app-store-connect-screenshots
66+
path: artifacts/app-store/screenshots/apple/
67+
if-no-files-found: warn
68+
retention-days: 14
69+
70+
android:
71+
name: Android phone, 7-inch tablet, and 10-inch tablet
72+
if: inputs.platform == 'all' || inputs.platform == 'android'
73+
runs-on: blacksmith-16vcpu-ubuntu-2404
74+
timeout-minutes: 60
75+
env:
76+
T3_SHOWCASE_ANDROID_ABI: x86_64
77+
steps:
78+
- name: Checkout
79+
uses: actions/checkout@v6
80+
81+
- name: Setup Vite+
82+
uses: voidzero-dev/setup-vp@v1
83+
with:
84+
node-version-file: package.json
85+
cache: true
86+
run-install: true
87+
88+
- name: Expose pnpm
89+
run: |
90+
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
91+
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
92+
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
93+
"$vp_pnpm_bin/pnpm" --version
94+
95+
- name: Setup Java
96+
uses: actions/setup-java@v5
97+
with:
98+
distribution: temurin
99+
java-version: 17
100+
101+
- name: Setup Gradle cache
102+
uses: gradle/actions/setup-gradle@v5
103+
104+
- name: Enable KVM
105+
run: |
106+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS="static_node=kvm"' \
107+
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
108+
sudo udevadm control --reload-rules
109+
sudo udevadm trigger --name-match=kvm
110+
111+
- name: Capture Android showcase
112+
uses: reactivecircus/android-emulator-runner@v2
113+
with:
114+
api-level: 36
115+
target: google_apis
116+
arch: x86_64
117+
profile: pixel_7_pro
118+
avd-name: Pixel_10_Pro
119+
cores: 8
120+
ram-size: 4096M
121+
disable-animations: false
122+
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}"
123+
124+
- name: Validate Google Play assets
125+
run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --validate-only
126+
127+
- name: Upload Android screenshots
128+
if: always()
129+
uses: actions/upload-artifact@v7
130+
with:
131+
name: google-play-screenshots
132+
path: artifacts/app-store/screenshots/google-play/
133+
if-no-files-found: warn
134+
retention-days: 14

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ squashfs-root/
2727
.gstack/
2828
dist-electron/
2929
.electron-runtime/
30+
.showcase/
31+
apps/mobile/.showcase/
32+
artifacts/app-store/screenshots/
3033
node_modules/
3134
.alchemy/
3235
*.log

apps/desktop/scripts/electron-launcher.mjs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment
2020
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
2121
: "com.t3tools.t3code";
2222
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
23-
const LAUNCHER_VERSION = 12;
23+
const LAUNCHER_VERSION = 14;
2424
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
2525
const developmentMacIconPngPath = NodePath.join(
2626
repoRoot,
@@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {
220220
}
221221
}
222222

223-
function patchMainBundleInfoPlist(appBundlePath, iconPath) {
223+
function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
224224
const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
225225
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
226226
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
227227
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
228+
setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
228229
setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
229230
setPlistJson(infoPlistPath, "CFBundleURLTypes", [
230231
{
@@ -277,11 +278,25 @@ function readJson(path) {
277278
}
278279
}
279280

281+
export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {
282+
const executableDir = NodePath.join(appBundlePath, "Contents", "MacOS");
283+
const launcherExecutableName = `${displayName} Launcher`;
284+
return {
285+
launcherExecutableName,
286+
launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName),
287+
runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"),
288+
};
289+
}
290+
280291
function buildMacLauncher(electronBinaryPath) {
281292
const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");
282293
const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");
283294
const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
284-
const targetBinaryPath = NodePath.join(targetAppBundlePath, "Contents", "MacOS", "Electron");
295+
const developmentPaths = resolveMacLauncherPaths(targetAppBundlePath);
296+
const runtimeElectronBinaryPath = developmentPaths.runtimeElectronBinaryPath;
297+
const launcherBinaryPath = isDevelopment
298+
? developmentPaths.launcherBinaryPath
299+
: runtimeElectronBinaryPath;
285300
const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
286301
const metadataPath = NodePath.join(runtimeDir, "metadata.json");
287302

@@ -298,18 +313,19 @@ function buildMacLauncher(electronBinaryPath) {
298313

299314
const currentMetadata = readJson(metadataPath);
300315
if (
301-
NodeFS.existsSync(targetBinaryPath) &&
316+
NodeFS.existsSync(launcherBinaryPath) &&
317+
(!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) &&
302318
currentMetadata &&
303319
JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
304320
) {
305321
if (isDevelopment) {
306322
// The launcher also handles protocol activations outside the dev runner,
307323
// so refresh its fallback environment on every launch. Never let a value
308324
// captured by an older parent app override the live dev-runner environment.
309-
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
325+
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
310326
}
311327
registerMacLauncherBundle(targetAppBundlePath);
312-
return targetBinaryPath;
328+
return launcherBinaryPath;
313329
}
314330

315331
NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });
@@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {
321337
recursive: true,
322338
verbatimSymlinks: true,
323339
});
324-
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
340+
patchMainBundleInfoPlist(
341+
targetAppBundlePath,
342+
iconPath,
343+
isDevelopment ? developmentPaths.launcherExecutableName : "Electron",
344+
);
325345
patchHelperBundleInfoPlists(targetAppBundlePath);
326346
if (isDevelopment) {
327-
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
347+
// Keep Electron's native executable inside the branded bundle. Launching the
348+
// node_modules copy makes macOS associate the process (and Dock label) with
349+
// Electron.app even though this bundle's Info.plist has the T3 Code name.
350+
// Its conventional executable name also keeps Electron's default-app runtime
351+
// in development mode instead of making app.isPackaged report true.
352+
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
328353
}
329354
NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
330355
registerMacLauncherBundle(targetAppBundlePath);
331356

332-
return targetBinaryPath;
357+
return launcherBinaryPath;
333358
}
334359

335360
function isLinuxSetuidSandboxConfigured(electronBinaryPath) {

apps/desktop/scripts/electron-launcher.test.mjs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { assert, describe, it } from "vite-plus/test";
22

3-
import { makeDevelopmentLauncherScript, resolveElectronBinaryPath } from "./electron-launcher.mjs";
3+
import {
4+
makeDevelopmentLauncherScript,
5+
resolveElectronBinaryPath,
6+
resolveMacLauncherPaths,
7+
} from "./electron-launcher.mjs";
48

59
describe("electron development launcher", () => {
610
it("uses captured values only as fallbacks for a live runner environment", () => {
@@ -45,4 +49,33 @@ describe("electron development launcher", () => {
4549
);
4650
assert.deepEqual(calls, ["ensure", "require:electron"]);
4751
});
52+
53+
it("keeps the native Electron executable name inside the branded macOS bundle", () => {
54+
const paths = resolveMacLauncherPaths(
55+
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",
56+
"T3 Code (Dev)",
57+
);
58+
59+
assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");
60+
assert.equal(
61+
paths.launcherBinaryPath,
62+
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher",
63+
);
64+
assert.equal(
65+
paths.runtimeElectronBinaryPath,
66+
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron",
67+
);
68+
69+
const script = makeDevelopmentLauncherScript({
70+
electronBinaryPath: paths.runtimeElectronBinaryPath,
71+
mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
72+
desktopRoot: "/repo/apps/desktop",
73+
environment: {},
74+
});
75+
assert.include(
76+
script,
77+
"exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'",
78+
);
79+
assert.notInclude(script, "node_modules/electron");
80+
});
4881
});

apps/desktop/src/electron/ElectronMenu.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ const TestLayer = ElectronMenu.layer.pipe(
3131
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
3232
);
3333

34+
const makeWindow = (zoomFactor = 1): Electron.BrowserWindow =>
35+
({
36+
id: 7,
37+
webContents: { getZoomFactor: () => zoomFactor },
38+
}) as unknown as Electron.BrowserWindow;
39+
3440
describe("ElectronMenu", () => {
3541
beforeEach(() => {
3642
buildFromTemplateMock.mockReset();
@@ -70,7 +76,7 @@ describe("ElectronMenu", () => {
7076

7177
const electronMenu = yield* ElectronMenu.ElectronMenu;
7278
const selectedItemId = yield* electronMenu.showContextMenu({
73-
window: {} as Electron.BrowserWindow,
79+
window: makeWindow(),
7480
items: [{ id: "copy", label: "Copy" }],
7581
position: Option.none(),
7682
});
@@ -81,20 +87,24 @@ describe("ElectronMenu", () => {
8187

8288
it.effect("resolves with none when the menu closes without a click", () =>
8389
Effect.gen(function* () {
90+
let popupOptions: Electron.PopupOptions | undefined;
8491
buildFromTemplateMock.mockImplementation(() => ({
8592
popup: (options: Electron.PopupOptions) => {
93+
popupOptions = options;
8694
options.callback?.();
8795
},
8896
}));
8997

9098
const electronMenu = yield* ElectronMenu.ElectronMenu;
9199
const selectedItemId = yield* electronMenu.showContextMenu({
92-
window: {} as Electron.BrowserWindow,
100+
window: makeWindow(2),
93101
items: [{ id: "copy", label: "Copy" }],
94102
position: Option.some({ x: 10.8, y: 20.2 }),
95103
});
96104

97105
assert.isTrue(Option.isNone(selectedItemId));
106+
assert.equal(popupOptions?.x, 21);
107+
assert.equal(popupOptions?.y, 40);
98108
assert.deepEqual(buildFromTemplateMock.mock.calls[0]?.[0][0], {
99109
label: "Copy",
100110
enabled: true,

apps/desktop/src/electron/ElectronUpdater.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { autoUpdaterMock } = vi.hoisted(() => ({
1010
autoInstallOnAppQuit: true,
1111
channel: "latest",
1212
disableDifferentialDownload: false,
13+
fullChangelog: false,
1314
checkForUpdates: vi.fn(() => Promise.resolve(null)),
1415
downloadUpdate: vi.fn(() => Promise.resolve([])),
1516
on: vi.fn(),
@@ -33,6 +34,7 @@ describe("ElectronUpdater", () => {
3334
autoUpdaterMock.autoInstallOnAppQuit = true;
3435
autoUpdaterMock.channel = "latest";
3536
autoUpdaterMock.disableDifferentialDownload = false;
37+
autoUpdaterMock.fullChangelog = false;
3638
autoUpdaterMock.checkForUpdates.mockClear();
3739
autoUpdaterMock.checkForUpdates.mockImplementation(() => Promise.resolve(null));
3840
autoUpdaterMock.downloadUpdate.mockClear();
@@ -98,6 +100,18 @@ describe("ElectronUpdater", () => {
98100
}).pipe(Effect.provide(ElectronUpdater.layer)),
99101
);
100102

103+
it.effect("sets full changelog mode", () =>
104+
Effect.gen(function* () {
105+
const updater = yield* ElectronUpdater.ElectronUpdater;
106+
107+
yield* updater.setFullChangelog(true);
108+
assert.equal(autoUpdaterMock.fullChangelog, true);
109+
110+
yield* updater.setFullChangelog(false);
111+
assert.equal(autoUpdaterMock.fullChangelog, false);
112+
}).pipe(Effect.provide(ElectronUpdater.layer)),
113+
);
114+
101115
it.effect("preserves quit-and-install flags and the execution-time channel", () =>
102116
Effect.gen(function* () {
103117
const cause = new Error("quit and install failed");

apps/desktop/src/electron/ElectronUpdater.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class ElectronUpdater extends Context.Service<
6666
readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
6767
readonly allowDowngrade: Effect.Effect<boolean>;
6868
readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
69+
readonly setFullChangelog: (value: boolean) => Effect.Effect<void>;
6970
readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
7071
readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
7172
readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;
@@ -112,6 +113,11 @@ export const make = ElectronUpdater.of({
112113
autoUpdater.allowDowngrade = value;
113114
return Effect.void;
114115
}),
116+
setFullChangelog: (value) =>
117+
Effect.suspend(() => {
118+
autoUpdater.fullChangelog = value;
119+
return Effect.void;
120+
}),
115121
setDisableDifferentialDownload: (value) =>
116122
Effect.suspend(() => {
117123
autoUpdater.disableDifferentialDownload = value;

0 commit comments

Comments
 (0)