Skip to content

Commit 5acb434

Browse files
authored
Merge branch 'tw93:main' into main
2 parents baa21b0 + 3f530fa commit 5acb434

25 files changed

Lines changed: 799 additions & 253 deletions

File tree

.github/actions/setup-env/action.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,20 @@ runs:
147147
}
148148
149149
# Build optimizations (caching)
150+
- name: Setup sccache
151+
if: inputs.mode == 'build'
152+
uses: mozilla-actions/sccache-action@v0.0.9
153+
154+
- name: Enable sccache
155+
if: inputs.mode == 'build'
156+
shell: bash
157+
run: |
158+
echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
159+
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
160+
150161
- name: Setup Rust cache
151162
if: inputs.mode == 'build'
152163
uses: swatinem/rust-cache@v2
153164
with:
154165
workspaces: "src-tauri -> target"
166+
shared-key: "pake-${{ runner.os }}"

.github/workflows/release.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ jobs:
4141
echo "apps_name=$(jq -c '[.[] | .name]' default_app_list.json)" >> $GITHUB_OUTPUT
4242
echo "apps_config=$(jq -c '.' default_app_list.json)" >> $GITHUB_OUTPUT
4343
44+
create-release:
45+
name: Create GitHub Release
46+
runs-on: ubuntu-latest
47+
if: startsWith(github.ref, 'refs/tags/')
48+
permissions:
49+
contents: write
50+
steps:
51+
- name: Create release placeholder
52+
uses: ncipollo/release-action@v1
53+
with:
54+
token: ${{ secrets.GITHUB_TOKEN }}
55+
skipIfReleaseExists: true
56+
4457
build-cli:
4558
name: Build CLI
4659
needs: release-apps
@@ -67,8 +80,11 @@ jobs:
6780

6881
build-popular-apps:
6982
name: ${{ matrix.config.title }}
70-
needs: [release-apps, build-cli]
71-
if: needs.release-apps.result == 'success' && needs.build-cli.result == 'success'
83+
needs: [release-apps, build-cli, create-release]
84+
if: |
85+
needs.release-apps.result == 'success' &&
86+
needs.build-cli.result == 'success' &&
87+
(needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
7288
strategy:
7389
matrix:
7490
config: ${{ fromJSON(needs.release-apps.outputs.apps_config) }}

.github/workflows/single-app.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ jobs:
335335
if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/')
336336
with:
337337
allowUpdates: true
338+
omitBody: true
339+
omitName: true
338340
artifacts: "output/linux/*.deb,output/linux/*.AppImage"
339341
token: ${{ secrets.GITHUB_TOKEN }}
340342

@@ -343,6 +345,8 @@ jobs:
343345
if: matrix.os == 'macos-latest' && startsWith(github.ref, 'refs/tags/')
344346
with:
345347
allowUpdates: true
348+
omitBody: true
349+
omitName: true
346350
artifacts: "output/macos/*.dmg"
347351
token: ${{ secrets.GITHUB_TOKEN }}
348352

@@ -351,5 +355,7 @@ jobs:
351355
if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
352356
with:
353357
allowUpdates: true
358+
omitBody: true
359+
omitName: true
354360
artifacts: "output/windows/*.msi"
355361
token: ${{ secrets.GITHUB_TOKEN }}

bin/defaults.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
4040
enableDragDrop: false,
4141
keepBinary: false,
4242
multiInstance: false,
43+
multiWindow: false,
4344
startToTray: false,
4445
forceInternalNavigation: false,
46+
internalUrlRegex: '',
4547
iterativeBuild: false,
4648
zoom: 100,
4749
minWidth: 0,

bin/helpers/cli-program.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
163163
.default(DEFAULT.multiInstance)
164164
.hideHelp(),
165165
)
166+
.addOption(
167+
new Option(
168+
'--multi-window',
169+
'Allow opening multiple windows within one app instance',
170+
)
171+
.default(DEFAULT.multiWindow)
172+
.hideHelp(),
173+
)
166174
.addOption(
167175
new Option('--start-to-tray', 'Start app minimized to tray')
168176
.default(DEFAULT.startToTray)
@@ -176,6 +184,14 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
176184
.default(DEFAULT.forceInternalNavigation)
177185
.hideHelp(),
178186
)
187+
.addOption(
188+
new Option(
189+
'--internal-url-regex <string>',
190+
'Regex pattern to match URLs that should be considered internal',
191+
)
192+
.default(DEFAULT.internalUrlRegex)
193+
.hideHelp(),
194+
)
179195
.addOption(
180196
new Option('--installer-language <string>', 'Installer language')
181197
.default(DEFAULT.installerLanguage)

bin/helpers/merge.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ export async function mergeConfig(
7070
wasm,
7171
enableDragDrop,
7272
multiInstance,
73+
multiWindow,
7374
startToTray,
7475
forceInternalNavigation,
76+
internalUrlRegex,
7577
zoom,
7678
minWidth,
7779
minHeight,
@@ -101,6 +103,7 @@ export async function mergeConfig(
101103
enable_drag_drop: enableDragDrop,
102104
start_to_tray: startToTray && showSystemTray,
103105
force_internal_navigation: forceInternalNavigation,
106+
internal_url_regex: internalUrlRegex,
104107
zoom,
105108
min_width: minWidth,
106109
min_height: minHeight,
@@ -370,6 +373,7 @@ Terminal=false
370373
}
371374
tauriConf.pake.proxy_url = proxyUrl || '';
372375
tauriConf.pake.multi_instance = multiInstance;
376+
tauriConf.pake.multi_window = multiWindow;
373377

374378
// Configure WASM support with required HTTP headers
375379
if (wasm) {

bin/options/icon.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getSpinner } from '@/utils/info';
1111
import { npmDirectory } from '@/utils/dir';
1212
import { IS_LINUX, IS_WIN, IS_MAC } from '@/utils/platform';
1313
import { PakeAppOptions } from '@/types';
14+
import { writeIcoWithPreferredSize } from '@/utils/ico';
1415

1516
type PlatformIconConfig = {
1617
format: string;
@@ -75,7 +76,15 @@ async function copyWindowsIconIfNeeded(
7576
try {
7677
const finalIconPath = generateIconPath(appName);
7778
await fsExtra.ensureDir(path.dirname(finalIconPath));
78-
await fsExtra.copy(convertedPath, finalIconPath);
79+
// Reorder ICO to prioritize 256px icons for better Windows display
80+
const reordered = await writeIcoWithPreferredSize(
81+
convertedPath,
82+
finalIconPath,
83+
256,
84+
);
85+
if (!reordered) {
86+
await fsExtra.copy(convertedPath, finalIconPath);
87+
}
7988
return finalIconPath;
8089
} catch (error) {
8190
logger.warn(

bin/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,18 @@ export interface PakeCliOptions {
9494
// Allow multiple instances, default false (single instance)
9595
multiInstance: boolean;
9696

97+
// Allow opening multiple windows in one app instance, default false
98+
multiWindow: boolean;
99+
97100
// Start app minimized to tray, default false
98101
startToTray: boolean;
99102

100103
// Force navigation to stay inside the Pake window even for external links
101104
forceInternalNavigation: boolean;
102105

106+
// Regex pattern to match URLs that should be considered internal
107+
internalUrlRegex: string;
108+
103109
// Initial page zoom level (50-200), default 100
104110
zoom: number;
105111

@@ -149,6 +155,7 @@ export interface WindowConfig {
149155
enable_drag_drop: boolean;
150156
start_to_tray: boolean;
151157
force_internal_navigation: boolean;
158+
internal_url_regex: string;
152159
zoom: number;
153160
min_width: number;
154161
min_height: number;
@@ -163,4 +170,5 @@ export interface PakeConfig {
163170
system_tray_path: string;
164171
proxy_url: string;
165172
multi_instance: boolean;
173+
multi_window: boolean;
166174
}

bin/utils/ico.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import path from 'path';
2+
import fsExtra from 'fs-extra';
3+
4+
const ICO_HEADER_SIZE = 6;
5+
const ICO_DIR_ENTRY_SIZE = 16;
6+
const ICO_TYPE_ICON = 1;
7+
8+
export type IcoEntry = {
9+
index: number;
10+
width: number;
11+
height: number;
12+
bitCount: number;
13+
bytesInRes: number;
14+
imageOffset: number;
15+
directory: Buffer;
16+
data: Buffer;
17+
};
18+
19+
function decodeDimension(value: number): number {
20+
return value === 0 ? 256 : value;
21+
}
22+
23+
function compareByPreferredSize(
24+
preferredSize: number,
25+
): (a: IcoEntry, b: IcoEntry) => number {
26+
return (a, b) => {
27+
const aSize = Math.max(a.width, a.height);
28+
const bSize = Math.max(b.width, b.height);
29+
30+
const aExact = aSize === preferredSize ? 0 : 1;
31+
const bExact = bSize === preferredSize ? 0 : 1;
32+
if (aExact !== bExact) return aExact - bExact;
33+
34+
const aDistance = Math.abs(aSize - preferredSize);
35+
const bDistance = Math.abs(bSize - preferredSize);
36+
if (aDistance !== bDistance) return aDistance - bDistance;
37+
38+
const aSmaller = aSize < preferredSize ? 1 : 0;
39+
const bSmaller = bSize < preferredSize ? 1 : 0;
40+
if (aSmaller !== bSmaller) return aSmaller - bSmaller;
41+
42+
if (a.bitCount !== b.bitCount) return b.bitCount - a.bitCount;
43+
if (aSize !== bSize) return bSize - aSize;
44+
45+
return a.index - b.index;
46+
};
47+
}
48+
49+
export function parseIcoBuffer(buffer: Buffer): IcoEntry[] {
50+
if (buffer.length < ICO_HEADER_SIZE) {
51+
throw new Error('Invalid ICO: header too short.');
52+
}
53+
54+
const reserved = buffer.readUInt16LE(0);
55+
const type = buffer.readUInt16LE(2);
56+
const count = buffer.readUInt16LE(4);
57+
58+
if (reserved !== 0 || type !== ICO_TYPE_ICON || count < 1) {
59+
throw new Error('Invalid ICO: invalid header.');
60+
}
61+
62+
const tableSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
63+
if (buffer.length < tableSize) {
64+
throw new Error('Invalid ICO: directory table too short.');
65+
}
66+
67+
const entries: IcoEntry[] = [];
68+
69+
for (let i = 0; i < count; i++) {
70+
const offset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;
71+
const widthByte = buffer.readUInt8(offset);
72+
const heightByte = buffer.readUInt8(offset + 1);
73+
const bitCount = buffer.readUInt16LE(offset + 6);
74+
const bytesInRes = buffer.readUInt32LE(offset + 8);
75+
const imageOffset = buffer.readUInt32LE(offset + 12);
76+
77+
if (bytesInRes < 1 || imageOffset + bytesInRes > buffer.length) {
78+
throw new Error('Invalid ICO: frame out of bounds.');
79+
}
80+
81+
entries.push({
82+
index: i,
83+
width: decodeDimension(widthByte),
84+
height: decodeDimension(heightByte),
85+
bitCount,
86+
bytesInRes,
87+
imageOffset,
88+
directory: buffer.subarray(offset, offset + ICO_DIR_ENTRY_SIZE),
89+
data: buffer.subarray(imageOffset, imageOffset + bytesInRes),
90+
});
91+
}
92+
93+
return entries;
94+
}
95+
96+
export function buildReorderedIcoBuffer(
97+
buffer: Buffer,
98+
preferredSize: number,
99+
): Buffer {
100+
const entries = parseIcoBuffer(buffer);
101+
const ordered = [...entries].sort(compareByPreferredSize(preferredSize));
102+
const count = ordered.length;
103+
const tableSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
104+
const payloadSize = ordered.reduce(
105+
(acc, entry) => acc + entry.data.length,
106+
0,
107+
);
108+
const output = Buffer.alloc(tableSize + payloadSize);
109+
110+
output.writeUInt16LE(0, 0);
111+
output.writeUInt16LE(ICO_TYPE_ICON, 2);
112+
output.writeUInt16LE(count, 4);
113+
114+
let currentOffset = tableSize;
115+
for (let i = 0; i < count; i++) {
116+
const entry = ordered[i];
117+
const entryOffset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;
118+
119+
entry.directory.copy(output, entryOffset, 0, 8);
120+
output.writeUInt32LE(entry.data.length, entryOffset + 8);
121+
output.writeUInt32LE(currentOffset, entryOffset + 12);
122+
entry.data.copy(output, currentOffset);
123+
currentOffset += entry.data.length;
124+
}
125+
126+
return output;
127+
}
128+
129+
export async function writeIcoWithPreferredSize(
130+
sourcePath: string,
131+
outputPath: string,
132+
preferredSize: number,
133+
): Promise<boolean> {
134+
try {
135+
const sourceBuffer = await fsExtra.readFile(sourcePath);
136+
const reordered = buildReorderedIcoBuffer(sourceBuffer, preferredSize);
137+
await fsExtra.ensureDir(path.dirname(outputPath));
138+
await fsExtra.outputFile(outputPath, reordered);
139+
return true;
140+
} catch {
141+
return false;
142+
}
143+
}

0 commit comments

Comments
 (0)