Skip to content

Commit c0f0df1

Browse files
mathix420claude
andcommitted
feat(windows): implement Windows install via PowerShell shortcut
Generates a PowerShell script that downloads the icon, wraps it as an .ico, configures the Firefox FTWA profile when relevant, and creates a Start Menu .lnk shortcut pointing at the browser with the right app arguments. Enables Windows in the OS picker and routes /u and /v requests through the new template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2218b99 commit c0f0df1

9 files changed

Lines changed: 112 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ When choosing linux as target OS, FTWA generate a shell script that will create
3232

3333
### On Windows
3434

35-
TODO: Not implemented yet. But will use shortcuts.
35+
FTWA generates a PowerShell script that downloads the icon, wraps it as an `.ico`, and creates a Start Menu `.lnk` shortcut (via `WScript.Shell`) pointing at the browser executable with the right `--app=` / Firefox profile arguments. The shortcut shows up in Start Menu search.
3636

3737
### On Macos
3838

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ onMounted(() => {
4949
path: "",
5050
}));
5151
targetInfos.value.bw = getUserBrowser(["safari"]);
52-
targetInfos.value.os = getUserOS(["windows"]);
52+
targetInfos.value.os = getUserOS([]);
5353
});
5454
5555
useSeoMeta({

app/components/AppUnchainer.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ async function submitWebsite() {
5353
});
5454
if (data.value?.uid) {
5555
websiteId.value = data.value.uid;
56-
const installUrl = getInstallURL({ id: data.value.uid }, false);
57-
execCode.value = `sh -ec "$(curl -fsSL '${installUrl}')"`;
56+
execCode.value = getInstallCommand({ id: data.value.uid }, false);
5857
}
5958
submitLoading.value = false;
6059
}

app/composables/copyCode.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import type { VerifiedWebsiteType } from "~~/types";
33
export function copyCode(website: VerifiedWebsiteType) {
44
const { copy } = useCopyToClipboard();
55

6-
const installURL = getInstallURL(website);
7-
const code = `sh -ec "$(curl -fsSL '${installURL}')"`;
6+
const code = getInstallCommand(website);
87

98
useRouter().replace({ hash: "#" });
109

app/composables/getInstalURL.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ export function getInstallURL(website: Pick<VerifiedWebsiteType, "id">, verified
99
+ `&bw=${targetInfos.value.bw}`
1010
+ `&path=${encodeURIComponent(targetInfos.value.path)}`;
1111
}
12+
13+
export function getInstallCommand(website: Pick<VerifiedWebsiteType, "id">, verified = true): string {
14+
const targetInfos = useState<TargetInfos>("target-infos");
15+
const url = getInstallURL(website, verified);
16+
17+
if (targetInfos.value.os === "windows") {
18+
return `powershell -NoProfile -Command "iex ((New-Object Net.WebClient).DownloadString('${url}'))"`;
19+
}
20+
return `sh -ec "$(curl -fsSL '${url}')"`;
21+
}

app/settings.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ export const operatingSystems: OperatingSystem[] = [{
3131
label: "macOS",
3232
icon: "simple-icons:apple",
3333
}, {
34-
disabled: true,
3534
value: "windows",
36-
label: "Windows (to be implemented)",
35+
label: "Windows",
3736
icon: "simple-icons:windows",
3837
}];
3938

@@ -85,7 +84,7 @@ export const browsers: TBrowser[] = [{
8584
}, {
8685
disabled: true,
8786
value: "safari",
88-
label: "Safari (to be implemented)",
87+
label: "Safari (cannot be implemented)",
8988
defaultExecutablePath: {
9089
linux: null,
9190
macos: "/Applications/Safari.app/Contents/MacOS/Safari",

server/routes/u/[uid].get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
1111
return "echo '\nERROR: Missing target infos.'";
1212
}
1313

14-
if (["windows"].includes(target.os) || ["safari"].includes(target.bw)) {
14+
if (["safari"].includes(target.bw)) {
1515
return "echo '\nERROR: Target unsupported (yet).'";
1616
}
1717

server/routes/v/[uid].get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
1111
return "echo '\nERROR: Missing target infos.'";
1212
}
1313

14-
if (["windows"].includes(target.os) || ["safari"].includes(target.bw)) {
14+
if (["safari"].includes(target.bw)) {
1515
return "echo '\nERROR: Target unsupported (yet).'";
1616
}
1717

server/templates/windows.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,105 @@
1+
import userChromeCss from "./files/userChrome.css";
2+
import userJs from "./files/user.js.txt";
13
import type { TargetInfos, WebsiteType } from "~~/types";
24

5+
function psQuote(s: string): string {
6+
return s.replace(/'/g, "''");
7+
}
8+
9+
function firefoxSetup(target: TargetInfos): string {
10+
if (target.bw !== "firefox") return "# Firefox setup skipped";
11+
12+
return `Write-Host "\nSetting up Firefox..."
13+
Start-Process -FilePath '${psQuote(target.path)}' -ArgumentList '-CreateProfile','FTWA' -Wait -NoNewWindow
14+
15+
$ProfileBase = Join-Path $env:APPDATA 'Mozilla\\Firefox\\Profiles'
16+
$ProfileFolder = Get-ChildItem -Path $ProfileBase -Directory -Filter '*.FTWA' | Select-Object -First 1 -ExpandProperty FullName
17+
if (-not $ProfileFolder) { throw 'FTWA Firefox profile not found.' }
18+
19+
$ChromeDir = Join-Path $ProfileFolder 'chrome'
20+
New-Item -ItemType Directory -Force -Path $ChromeDir | Out-Null
21+
22+
@'
23+
${userChromeCss}
24+
'@ | Set-Content -Path (Join-Path $ChromeDir 'userChrome.css') -Encoding UTF8
25+
26+
@'
27+
${userJs}
28+
'@ | Set-Content -Path (Join-Path $ProfileFolder 'user.js') -Encoding UTF8
29+
30+
Write-Host "Firefox setup done."`;
31+
}
32+
333
export function windows({
434
website,
535
target,
636
}: {
737
website: WebsiteType;
838
target: TargetInfos;
939
}) {
10-
// TODO: https://github.com/nwutils/create-desktop-shortcuts/blob/main/src/windows.vbs
40+
const safeName = website.name.replace(/[\\/:*?"<>|]/g, "");
41+
const fname = "ftwa-" + website.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
42+
43+
let commandArgs = "";
44+
if (target.bw === "firefox") {
45+
commandArgs = `--no-remote -P "FTWA" "${website.url}"`;
46+
} else if (["chrome", "opera", "brave", "edge"].includes(target.bw)) {
47+
commandArgs = `--app=${website.url}`;
48+
}
49+
50+
return `# Generated by FTWA (https://ftwa.mathix.dev)
51+
# /u/ links are only available for 24h
52+
# Check the app store for more apps: https://ftwa.mathix.dev/store
53+
54+
$ErrorActionPreference = 'Stop'
55+
56+
${firefoxSetup(target)}
57+
58+
Write-Host "\nCreating shortcut..."
59+
60+
$IconDir = Join-Path $env:LOCALAPPDATA 'FTWA\\icons'
61+
New-Item -ItemType Directory -Force -Path $IconDir | Out-Null
62+
63+
$PngPath = Join-Path $IconDir '${fname}.png'
64+
$IcoPath = Join-Path $IconDir '${fname}.ico'
65+
66+
Invoke-WebRequest -Uri '${website.logo}' -OutFile $PngPath -UseBasicParsing
67+
68+
# Wrap the PNG as a single-image ICO so Windows can use any size (incl. 256+)
69+
Add-Type -AssemblyName System.Drawing
70+
$Bmp = [System.Drawing.Bitmap]::FromFile($PngPath)
71+
$IcoW = if ($Bmp.Width -ge 256) { [byte]0 } else { [byte]$Bmp.Width }
72+
$IcoH = if ($Bmp.Height -ge 256) { [byte]0 } else { [byte]$Bmp.Height }
73+
$Bmp.Dispose()
74+
75+
$PngBytes = [System.IO.File]::ReadAllBytes($PngPath)
76+
$Ms = New-Object System.IO.MemoryStream
77+
$Bw = New-Object System.IO.BinaryWriter($Ms)
78+
$Bw.Write([uint16]0); $Bw.Write([uint16]1); $Bw.Write([uint16]1)
79+
$Bw.Write($IcoW); $Bw.Write($IcoH); $Bw.Write([byte]0); $Bw.Write([byte]0)
80+
$Bw.Write([uint16]1); $Bw.Write([uint16]32)
81+
$Bw.Write([uint32]$PngBytes.Length); $Bw.Write([uint32]22)
82+
$Bw.Write($PngBytes)
83+
$Bw.Close()
84+
[System.IO.File]::WriteAllBytes($IcoPath, $Ms.ToArray())
85+
$Ms.Dispose()
86+
87+
$BrowserPath = '${psQuote(target.path)}'
88+
if (-not (Test-Path $BrowserPath)) { throw "Browser not found: $BrowserPath" }
89+
90+
$ShortcutDir = [Environment]::GetFolderPath('Programs')
91+
$ShortcutPath = Join-Path $ShortcutDir '${psQuote(safeName)}.lnk'
92+
93+
$WshShell = New-Object -ComObject WScript.Shell
94+
$Shortcut = $WshShell.CreateShortcut($ShortcutPath)
95+
$Shortcut.TargetPath = $BrowserPath
96+
$Shortcut.Arguments = '${psQuote(commandArgs)}'
97+
$Shortcut.IconLocation = $IcoPath
98+
$Shortcut.Description = 'Generated by FTWA (https://ftwa.mathix.dev)'
99+
$Shortcut.WorkingDirectory = (Split-Path $BrowserPath -Parent)
100+
$Shortcut.Save()
11101
12-
return "";
102+
Write-Host "Shortcut created: $ShortcutPath"
103+
Write-Host "Search for '${psQuote(website.name)}' in the Start Menu."
104+
`;
13105
}

0 commit comments

Comments
 (0)