Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions chat/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,8 @@
"settings.browserOptionReset": "Reset to default",
"settings.browserOptionSave": "Save",
"settings.browserOptionTitle": "Browser Path",
"settings.alwaysOpenIncognito": "Always open links in incognito/private mode",
"settings.alwaysOpenIncognito.help": "When enabled, left-clicking any URL link in chat opens it in your browser's incognito/private window instead of a normal tab. Also applies when opening an image from the profile viewer or the profile itself in your browser.",
"settings.cache": "Profile cache",
"settings.cache.expiryDays": "Maximum age",
"settings.cache.expiryDays.description": "The amount of days that a profile's data will be kept on your computer. If a profile's data hasn't been used since then, it will be cleared to make space.",
Expand Down
4 changes: 2 additions & 2 deletions electron/About.vue
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@

<script lang="ts">
import * as remote from '@electron/remote';
import { clipboard, shell } from 'electron';
import { clipboard, ipcRenderer, shell } from 'electron';
import Vue from 'vue';
import l, { setLanguage } from '../chat/localize';
import LocalizedText from '../components/localized_text';
Expand Down Expand Up @@ -507,7 +507,7 @@
url = `${base}?template=bug.yml`;
}
try {
void shell.openExternal(url);
ipcRenderer.send('open-url-externally', url);
} catch (e) {
console.warn('Failed to open issue page', e);
}
Expand Down
1 change: 0 additions & 1 deletion electron/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,6 @@
'open-url-externally',
`https://www.f-list.net/c/${this.profileName}`
);
//await remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);

// tslint:disable-next-line: no-any no-unsafe-any
(this.$refs.profileViewer as any).hide();
Expand Down
19 changes: 19 additions & 0 deletions electron/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,25 @@
</p>
</small>
</label>

<div class="mb-3" v-if="!isMac">
<label class="control-label" for="alwaysOpenIncognito">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="alwaysOpenIncognito"
v-model="settings.horizonAlwaysOpenIncognito"
/>
<label class="form-check-label" for="alwaysOpenIncognito">
{{ l('settings.alwaysOpenIncognito') }}
</label>
</div>
<small class="form-text text-muted">{{
l('settings.alwaysOpenIncognito.help')
}}</small>
</label>
</div>
<h5>
{{
l('settings.experimental', {
Expand Down
2 changes: 1 addition & 1 deletion electron/browser_windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,7 @@ export function createAboutWindow(

// Handle external links
about.webContents.setWindowOpenHandler(({ url }) => {
electron.shell.openExternal(url);
openURLExternally(url);
return { action: 'deny' };
});

Expand Down
251 changes: 14 additions & 237 deletions electron/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,9 @@ const webContents = remote.getCurrentWebContents();
require('@electron/remote/main').enable(webContents);

import Axios from 'axios';
import { exec, execFileSync, execSync, spawn } from 'child_process';
import { exec } from 'child_process';
import * as path from 'path';
import * as qs from 'querystring';
import * as fs from 'fs';
import { getKey } from '../chat/common';
import { EventBus } from '../chat/preview/event-bus';
import { init as initCore } from '../chat/core';
Expand Down Expand Up @@ -108,230 +107,6 @@ if (process.env.NODE_ENV === 'production') {
console.log(`%c${l('consoleWarning.body')}`, 'font-size: 16pt; color:red');
});
}
let browser: string | undefined;
let executablePath: string | undefined;

function openIncognitoWindows(url: string): void {
if (settings.browserPath && settings.browserPath.length > 0) {
executablePath = settings.browserPath;
log.debug('incognito.open.customPath', executablePath);
} else if (executablePath === undefined) {
//Default to Edge path
executablePath =
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe';
if (browser === undefined)
try {
//tslint:disable-next-line:max-line-length
browser = execSync(
`FOR /F "skip=2 tokens=3" %A IN ('REG QUERY HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice /v ProgId') DO @(echo %A)`
)
.toString()
.trim();
} catch (e) {
console.error(e);
}

log.debug('incognito.open.win32.ProgId', browser);

try {
let ftypeAssocmd = execSync(`ftype ${browser}`).toString().trim();
log.verbose('incognito.open.win32.ftype', ftypeAssocmd);
let match = ftypeAssocmd.match(/"([^"]+\.exe)"/);
if (match?.[1]) executablePath = match?.[1];
} catch (e) {
log.error('incognito.open.win32.ftype.error', e);

// Fallback: Query registry directly for modern ProgIds (like Vivaldi)
try {
const regQuery = execSync(
`REG QUERY "HKEY_CLASSES_ROOT\\${browser}\\shell\\open\\command" /ve`,
{ encoding: 'utf8' }
).toString();
const regMatch = regQuery.match(/"([^"]+\.exe)"/);
if (regMatch?.[1]) {
executablePath = regMatch[1];
log.debug('incognito.open.win32.registry.fallback', executablePath);
}
} catch (regError) {
log.error('incognito.open.win32.registry.error', regError);
}
}

log.debug('incognito.open.exePath', executablePath);
}
//"Everything is chrome in the future!" -🧽
let incognitoArg: string = '-incognito';

switch (path.basename(executablePath).toLowerCase()) {
case 'chrome.exe':
case 'chromeapp.exe':
case 'brave.exe':
case 'vivaldi.exe':
incognitoArg = '-incognito';
break;
case 'msedge.exe':
incognitoArg = '-inprivate';
break;
case 'firefox.exe':
case 'floorp.exe':
case 'librewolf.exe':
case 'waterfox.exe':
case 'palemoon.exe':
case 'zen.exe':
incognitoArg = '-private-window';
break;
case 'opera.exe':
incognitoArg = '-private';
break;
}

spawn(executablePath, [incognitoArg, url]);
}

// --- Linux incognito helpers ------------------------------------------------
// Resolve an executable to an absolute path using PATH; Otherwise, return the
// original command.
function resolveExecutable(cmd: string): string {
if (path.isAbsolute(cmd)) return cmd;
try {
const resolved = execFileSync('which', [cmd], { encoding: 'utf8' })
.trim()
.split('\n')[0];
if (resolved) return resolved;
} catch {
// Ignore: we'll try spawn with the raw token. Oh god.
}
return cmd;
}

const LINUX_INCOGNITO_FLAGS: Record<string, string> = {
// Chromium family
chrome: '--incognito',
'google-chrome': '--incognito',
'google-chrome-stable': '--incognito',
'google-chrome-beta': '--incognito',
'google-chrome-unstable': '--incognito',
chromium: '--incognito',
'chromium-browser': '--incognito',
brave: '--incognito',
'brave-browser': '--incognito',
vivaldi: '--incognito',
'vivaldi-stable': '--incognito',
'vivaldi-snapshot': '--incognito',

// why is edge on linux lol
'microsoft-edge': '--inprivate',
'microsoft-edge-stable': '--inprivate',
'microsoft-edge-beta': '--inprivate',
'microsoft-edge-dev': '--inprivate',

// Firefox family (default behaviour, kept explicit)
firefox: '-private-window',
'firefox-bin': '-private-window',
'firefox-esr': '-private-window',
librewolf: '-private-window',
waterfox: '-private-window',
palemoon: '-private-window',
zen: '-private-window',
icecat: '-private-window',
floorp: '-private-window',

// Opera
opera: '--private',
'opera-beta': '--private',
'opera-developer': '--private'
};

function extractExecCommand(desktopFilePath: string): string | undefined {
try {
const desktopFile = fs.readFileSync(desktopFilePath, { encoding: 'utf8' });
const match = desktopFile.match(/^\s*Exec\s*=\s*(.+)$/m);
if (!match) return undefined;

// Remove desktop entry field codes (%u, %U, etc.) per spec.
const execValue = match[1].replace(/%[a-zA-Z]/g, '').trim();
const tokens = execValue.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];

return tokens.find(
token =>
token !== 'env' &&
!/^[A-Za-z_][A-Za-z0-9_]*=/.test(token) &&
token.length > 0
);
} catch (err) {
log.error('incognito.open.linux.exec.read.error', desktopFilePath, err);
return undefined;
}
}

function openIncognitoLinux(url: string): void {
let desktopId: string | undefined;
try {
desktopId = execFileSync('xdg-settings', ['get', 'default-web-browser'], {
encoding: 'utf8'
}).trim();
} catch (err) {
log.error('incognito.open.linux.desktopId.error', err);
}

// & oh my god.
const candidatePaths =
desktopId === undefined
? []
: [
// user locations because these files take priority in freedesktop spec
path.join(
remote.app.getPath('home'),
'.local/share/applications',
desktopId
),
path.join(
remote.app.getPath('home'),
'.local/share/flatpak/exports/share/applications',
desktopId
),
// system locations
path.join('/usr/local/share/applications', desktopId),
path.join('/usr/share/applications', desktopId),

// flatpak and snap system locations (???)
path.join(
'/usr/local/share/flatpak/exports/share/applications',
desktopId
),
path.join('/var/lib/flatpak/exports/share/applications', desktopId),
// ? does snap live here or am i fucking pedantic
path.join('/var/lib/snapd/desktop/applications', desktopId)
];
// okay we're out of hell.
const desktopFilePath = candidatePaths.find(p => fs.existsSync(p));
let browserCommand = desktopFilePath
? extractExecCommand(desktopFilePath)
: undefined;

if (!browserCommand) {
log.warn(
'incognito.open.linux.fallback',
desktopId,
desktopFilePath ?? 'no desktop file found'
);

// ! Last resort:
// ! We are in hell. Hope that god has given us Firefox.
browserCommand = 'firefox';
}

const resolvedCommand = resolveExecutable(browserCommand);

log.debug('incognito.open.linux.browserCommand', resolvedCommand);

const incognitoArg =
LINUX_INCOGNITO_FLAGS[path.basename(browserCommand).toLowerCase()] ??
'-private-window'; // again, we pray to god we have firefox. :)

spawn(resolvedCommand, [incognitoArg, url]);
}

const wordPosSearch = new WordPosSearch();

webContents.on('context-menu', (_, props) => {
Expand Down Expand Up @@ -399,7 +174,12 @@ webContents.on('context-menu', (_, props) => {
id: 'openLink',
label: l('action.openBrowser'),
click(): void {
electron.ipcRenderer.send('open-url-externally', props.linkURL);
// explicit opt-out. always a normal window, even with always-incognito on
electron.ipcRenderer.send(
'open-url-externally',
props.linkURL,
'normal'
);
}
});
menuTemplate.push({
Expand All @@ -412,19 +192,16 @@ webContents.on('context-menu', (_, props) => {
}
});

if (process.platform === 'win32')
if (process.platform === 'win32' || process.platform === 'linux')
menuTemplate.push({
id: 'incognito',
label: l('action.incognito'),
click: () => openIncognitoWindows(props.linkURL)
});
else if (process.platform === 'linux')
menuTemplate.push({
id: 'incognito',
label: l('action.incognito'),
click: () => {
openIncognitoLinux(props.linkURL);
}
click: () =>
electron.ipcRenderer.send(
'open-url-externally',
props.linkURL,
'incognito'
)
});
} else if (hasText)
menuTemplate.push({
Expand Down
1 change: 1 addition & 0 deletions electron/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class GeneralSettings {
risingDisableWindowsHighContrast = false;
browserPath = '';
browserArgs = '%s';
horizonAlwaysOpenIncognito: boolean = false;
zoomLevel = 0.0;
horizonCustomCss: string = '';
horizonCustomCssEnabled: boolean = false;
Expand Down
Loading
Loading