Skip to content
Merged
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
10 changes: 7 additions & 3 deletions apps/desktop/extensions/ai-sidebar/newtab.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ const DEFAULT_TICKERS = 'SPY, AAPL, NVDA, BTC-USD';
const DEFAULT_LEAGUES = 'nfl, nba';
const splitList = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean);

// --- Search: Web (Kagi by default) or AI (sidebar) ---
// --- Search: Web (DuckDuckGo by default) or AI (sidebar) ---
// This is the NEW-TAB box only. The address bar's engine is a browser-level
// setting the extension cannot reach; `tron search <engine>` sets that one.
// Default is DuckDuckGo because it answers without an account — Kagi is
// subscription-only past its trial, so it cannot be the out-of-box default.
const SEARCH_ENGINES = {
kagi: { name: 'Kagi', url: 'https://kagi.com/search?q=' },
neosearch: { name: 'NeoSearch', url: 'https://neosearch.org/?q=' },
Expand All @@ -37,15 +41,15 @@ const TOR_SEARCH_ENGINES = {
excavator: { name: 'Excavator', url: 'http://2fd6cemt4gmccflhm6imvdfvli3nf7zn6rfrwpsy7uhxrgbypvwf5fad.onion/?q=' },
};
let searchMode = 'web';
let searchEngine = 'kagi';
let searchEngine = 'ddg';
let torSearchEngine = 'ahmia';
let torEnabled = false;

// Resolve the engine to use right now: the Tor default while the onion toggle
// is on, otherwise the clearnet default.
function activeEngine() {
if (torEnabled) return TOR_SEARCH_ENGINES[torSearchEngine] || TOR_SEARCH_ENGINES.ahmia;
return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.kagi;
return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.ddg;
}

function setMode(mode) {
Expand Down
11 changes: 6 additions & 5 deletions apps/desktop/extensions/ai-sidebar/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,16 @@ <h2>bittorrented.com — Live TV, Radio &amp; Podcasts</h2>
<span id="btrMsg" class="saved"></span>

<h2>Search</h2>
<p class="hint">Default search engine for the new-tab search box. Kagi is the
default; NeoSearch, Xprivo, DuckDuckGo, and others are available as alternatives. (The omnibox default
is set separately in <code>chrome://settings/search</code>.)</p>
<p class="hint">This sets the <strong>new-tab search box</strong> only.
The address bar is a browser-level setting this page cannot reach — change that
one with <code>tron search &lt;engine&gt;</code>, or in
<code>chrome://settings/search</code>.</p>
<label for="searchEngine">New-tab search engine (clearnet)</label>
<select id="searchEngine">
<option value="kagi">Kagi (default)</option>
<option value="ddg">DuckDuckGo (default)</option>
<option value="kagi">Kagi (needs a Kagi subscription)</option>
<option value="neosearch">NeoSearch (private)</option>
<option value="xprivo">Xprivo (private)</option>
<option value="ddg">DuckDuckGo</option>
<option value="altpower">Altpower</option>
<option value="oxiverse">Oxiverse</option>
</select>
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/extensions/ai-sidebar/settings-sections.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) {
const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']);

// Populate current values (on every mount, e.g. after a cloud pull).
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'kagi';
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg';
if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia';
if (el('tickers')) el('tickers').value = cur.tickers ?? '';
if (el('leagues')) el('leagues').value = cur.leagues ?? '';
Expand Down
99 changes: 83 additions & 16 deletions apps/desktop/launcher/tronbrowser
Original file line number Diff line number Diff line change
Expand Up @@ -186,29 +186,96 @@ if flag not in exp:
PY
fi

# Default the omnibox to Kagi once per profile, then respect the user's
# chrome://settings/search choice. The sentinel filename carries the engine so
# that changing the default here re-applies once on profiles created earlier.
if command -v python3 >/dev/null 2>&1 && [ ! -f "$DATA/.tron-search-kagi" ]; then
TB_PREFS="$DATA/Default/Preferences" python3 - <<'PY' 2>/dev/null || true
import json, os
# --- Omnibox search engine -------------------------------------------------
# The address bar's engine is a browser-level setting. An MV3 extension cannot
# change it at runtime and manifest `chrome_settings_overrides` isn't available
# on Linux, so the launcher is the only thing here that can set it — which is
# why `tron search <engine>` writes a file we read rather than the extension
# doing it. The new-tab box keeps its own picker in Settings; the two are
# separate on purpose and both say so in their UI.
#
# No suggestions_url anywhere below. A suggest endpoint fires a request on every
# keystroke in the address bar, which both leaks the query before you press
# enter and stalls typing when the endpoint is slow or answers 401.
search_engine_spec() {
case "$1" in
ddg) echo "DuckDuckGo|duckduckgo.com|https://duckduckgo.com/?q={searchTerms}" ;;
kagi) echo "Kagi|kagi.com|https://kagi.com/search?q={searchTerms}" ;;
neosearch) echo "NeoSearch|neosearch.org|https://neosearch.org/?q={searchTerms}" ;;
xprivo) echo "Xprivo|xprivo.com|https://www.xprivo.com/search/?q={searchTerms}" ;;
oxiverse) echo "Oxiverse|search.oxiverse.com|https://search.oxiverse.com/?q={searchTerms}&tab=web" ;;
# Altpower is deliberately absent: its query lives in the URL fragment, which
# never reaches a server as a search. It works from the new-tab box, which
# builds the URL itself, but not as an omnibox engine.
*) return 1 ;;
esac
}

# What the user asked for (`tron search <engine>`), or a default that works
# without an account. Kagi is subscription-only past its trial, so defaulting
# the omnibox to it leaves a fresh install unable to search at all.
SEARCH_WANT="$(cat "$DATA/search-engine" 2>/dev/null || true)"
SEARCH_WANT="$(printf '%s' "$SEARCH_WANT" | tr -d '[:space:]')"
SEARCH_EXPLICIT=1
if [ -z "$SEARCH_WANT" ]; then SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0; fi
if ! search_engine_spec "$SEARCH_WANT" >/dev/null 2>&1; then
echo "TronBrowser: unknown search engine '$SEARCH_WANT' — using ddg. See 'tron search'." >&2
SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0
fi

# Which engine we last set. Profiles from before this file existed recorded it
# in the sentinel's NAME; treat that as "we set Kagi" so the repair below can
# recognise its own handiwork.
SEARCH_MARK="$DATA/.tron-search"
SEARCH_PREV="$(cat "$SEARCH_MARK" 2>/dev/null || true)"
SEARCH_PREV="$(printf '%s' "$SEARCH_PREV" | tr -d '[:space:]')"
if [ -z "$SEARCH_PREV" ] && [ -f "$DATA/.tron-search-kagi" ]; then SEARCH_PREV="kagi"; fi

if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; then
SEARCH_PREV_URL=""
if [ -n "$SEARCH_PREV" ]; then
SEARCH_PREV_URL="$(search_engine_spec "$SEARCH_PREV" 2>/dev/null | cut -d'|' -f3 || true)"
fi
TB_PREFS="$DATA/Default/Preferences" \
TB_ENGINE="$(search_engine_spec "$SEARCH_WANT")" \
TB_PREV_URL="$SEARCH_PREV_URL" \
TB_FORCE="$SEARCH_EXPLICIT" \
python3 - <<'PY' 2>/dev/null || true
import json, os, tempfile
p = os.environ["TB_PREFS"]
name, keyword, url = os.environ["TB_ENGINE"].split("|")
prev_url = os.environ.get("TB_PREV_URL", "")
force = os.environ.get("TB_FORCE") == "1"
os.makedirs(os.path.dirname(p), exist_ok=True)
try:
d = json.load(open(p)) if os.path.exists(p) else {}
except Exception:
d = {}
d.setdefault("default_search_provider_data", {})["template_url_data"] = {
"short_name": "Kagi",
"keyword": "kagi.com",
"url": "https://kagi.com/search?q={searchTerms}",
"suggestions_url": "https://kagi.com/api/autosuggest?q={searchTerms}",
"favicon_url": "https://kagi.com/favicon.ico",
"safe_for_autoreplace": False,
}
json.dump(d, open(p, "w"))

# Only ever overwrite an engine that is absent or that we put there ourselves.
# Without this, changing the default below would stomp the choice of everyone
# who had already picked their own in chrome://settings/search. An explicit
# `tron search` is the one case that outranks whatever is currently set.
cur = d.get("default_search_provider_data", {}).get("template_url_data") or {}
cur_url = cur.get("url", "")
if force or not cur_url or (prev_url and cur_url == prev_url):
d.setdefault("default_search_provider_data", {})["template_url_data"] = {
"short_name": name,
"keyword": keyword,
"url": url,
"favicon_url": "https://" + keyword.split("/")[0] + "/favicon.ico",
"safe_for_autoreplace": False,
}
# Preferences is the profile. A truncated write loses every setting in it,
# so land it as a rename rather than in place.
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p))
with os.fdopen(fd, "w") as f:
json.dump(d, f)
os.replace(tmp, p)
PY
mkdir -p "$DATA"; : > "$DATA/.tron-search-kagi"
mkdir -p "$DATA"
printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true
rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true
fi

# Show the feed (ai-sidebar New Tab page) on startup. We can't pass a startup URL —
Expand Down
110 changes: 110 additions & 0 deletions apps/desktop/test/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,116 @@ describe('launcher flags', () => {
});
});

// --- Omnibox search engine ---------------------------------------------------
// The launcher is the only thing that can set Chromium's default search engine
// (no MV3 API for it, and chrome_settings_overrides isn't available on Linux),
// so the rules about when it may overwrite one live here.

const KAGI_URL = 'https://kagi.com/search?q={searchTerms}';

const prefsPath = (home: string) => join(home, 'profile', 'Default', 'Preferences');

/** The default_search_provider_data the launcher left in the profile. */
function dse(home: string): Record<string, string> {
const p = prefsPath(home);
if (!existsSync(p)) return {};
const json = JSON.parse(readFileSync(p, 'utf8'));
return json.default_search_provider_data?.template_url_data ?? {};
}

/** A profile that already has a search engine set, and a record of who set it. */
function seed(opts: { url?: string; marker?: string; legacyKagi?: boolean; want?: string }): string {
const home = mkdtempSync(join(tmpdir(), 'tron-launcher-'));
homes.push(home);
const data = join(home, 'profile');
mkdirSync(join(data, 'Default'), { recursive: true });
if (opts.url) {
writeFileSync(
prefsPath(home),
JSON.stringify({
default_search_provider_data: { template_url_data: { short_name: 'Seeded', url: opts.url } },
// A real profile has more than the one key; it must survive our write.
bookmark_bar: { show_on_all_tabs: true },
}),
);
}
if (opts.legacyKagi) writeFileSync(join(data, '.tron-search-kagi'), '');
if (opts.marker) writeFileSync(join(data, '.tron-search'), `${opts.marker}\n`);
if (opts.want) writeFileSync(join(data, 'search-engine'), `${opts.want}\n`);
return home;
}

describe('omnibox search engine', () => {
it('defaults a fresh profile to an engine that works without an account', () => {
// Kagi is subscription-only past its trial, so defaulting to it left a new
// install unable to search at all — it just landed on a login wall.
const { home } = run([]);
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
});

it('never sets a suggestions_url', () => {
// A suggest endpoint fires per keystroke in the address bar: it leaks the
// query before you hit enter, and stalls typing when it is slow or 401s.
const { home } = run([]);
expect(dse(home)).not.toHaveProperty('suggestions_url');
});

it('honors the engine chosen with `tron search`', () => {
const home = seed({ want: 'neosearch' });
run([], { home });
expect(dse(home).url).toBe('https://neosearch.org/?q={searchTerms}');
});

it('falls back and says so when the chosen engine is unknown', () => {
const home = seed({ want: 'notanengine' });
const { stderr } = run([], { home });
expect(stderr).toContain("unknown search engine 'notanengine'");
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
});

it('repairs a profile it had previously pinned to Kagi', () => {
// The actual bug: everyone who installed before this had Kagi written into
// their profile by us, and nothing moved them off it.
const home = seed({ url: KAGI_URL, legacyKagi: true });
run([], { home });
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
});

it('leaves an engine the user picked themselves alone', () => {
// We may correct our own default. We may not overwrite a deliberate choice.
const chosen = 'https://www.google.com/search?q={searchTerms}';
const home = seed({ url: chosen, legacyKagi: true });
run([], { home });
expect(dse(home).url).toBe(chosen);
});

it('lets an explicit `tron search` override even a user-set engine', () => {
const home = seed({ url: 'https://www.google.com/search?q={searchTerms}', want: 'kagi' });
run([], { home });
expect(dse(home).url).toBe(KAGI_URL);
});

it('stops touching the setting once it has applied it', () => {
// Second launch must not re-apply, or changing the engine in
// chrome://settings/search would be undone on every start.
const first = run([]);
const chosen = 'https://www.startpage.com/sp/search?q={searchTerms}';
writeFileSync(
prefsPath(first.home),
JSON.stringify({ default_search_provider_data: { template_url_data: { url: chosen } } }),
);
run([], { home: first.home });
expect(dse(first.home).url).toBe(chosen);
});

it('keeps the rest of Preferences when it rewrites the engine', () => {
const home = seed({ url: KAGI_URL, legacyKagi: true });
run([], { home });
const json = JSON.parse(readFileSync(prefsPath(home), 'utf8'));
expect(json.bookmark_bar?.show_on_all_tabs).toBe(true);
});
});

describe('engine reporting', () => {
it('names the engine it is about to run', () => {
const { stderr } = run([], { version: 'Chromium 141.0.0.0' });
Expand Down
34 changes: 34 additions & 0 deletions apps/web/public/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Usage:
tron replay <bundle> Replay a recorded trace against the session
tron upgrade Update to the latest release
tron clean Clear browser caches (keeps bookmarks, logins, history)
tron search [engine] Show or set the ADDRESS BAR's search engine
(the new-tab box is set in TronBrowser Settings)
tron remove Uninstall TronBrowser (keeps your profile data)
tron version Print the installed version
tron help Show this help
Expand Down Expand Up @@ -268,6 +270,38 @@ case "${1:-}" in
rm -rf "$_data/Default/Cache" "$_data/Default/Code Cache" "$_data/Default/Service Worker"
echo "Freed ~${_freed}MB from $_data. Bookmarks, passwords and logins untouched."
done ;;
search)
# The ADDRESS BAR's engine. Chromium reads it from the profile at startup and
# only the launcher can write it there — an MV3 extension has no API for this
# on Linux — so this records the choice and the next launch applies it. The
# new-tab box has its own separate picker in TronBrowser Settings.
_engines="ddg kagi neosearch xprivo oxiverse"
_dirs="${TRONBROWSER_DATA:-$HOME/.tronbrowser}"
if [ -d "$HOME/TronBrowser" ]; then _dirs="$_dirs $HOME/TronBrowser"; fi
_want="${2:-}"
if [ -z "$_want" ]; then
for _d in $_dirs; do
_cur="$(cat "$_d/search-engine" 2>/dev/null || true)"
echo "Address bar: ${_cur:-ddg (default)} [$_d]"
done
echo "Available: $_engines"
echo "Set with: tron search ddg"
echo "(The new-tab search box is set separately in TronBrowser Settings.)"
exit 0
fi
_ok=0
for _e in $_engines; do
if [ "$_e" = "$_want" ]; then _ok=1; fi
done
if [ "$_ok" != "1" ]; then
echo "tron search: unknown engine '$_want'. Available: $_engines" >&2
exit 1
fi
for _d in $_dirs; do
mkdir -p "$_d"
printf '%s\n' "$_want" > "$_d/search-engine"
done
echo "Address bar set to '$_want'. Restart TronBrowser to apply ('tron restart')." ;;
remove|uninstall)
rm -rf "$APP_DIR"
rm -f "$PREFIX/bin/tron" "$PREFIX/bin/tronbrowser" "$PREFIX/share/applications/tronbrowser.desktop"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/public/settings-sections.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) {
const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']);

// Populate current values (on every mount, e.g. after a cloud pull).
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'kagi';
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg';
if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia';
if (el('tickers')) el('tickers').value = cur.tickers ?? '';
if (el('leagues')) el('leagues').value = cur.leagues ?? '';
Expand Down
9 changes: 6 additions & 3 deletions apps/web/public/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@ <h2>API keys (bring your own)</h2>

<!-- Shared sections (identical to the extension options page, via settings-sections.js) -->
<h2>Search</h2>
<p class="hint">Default search engine for the new-tab search box.</p>
<p class="hint">This sets the <strong>new-tab search box</strong> only.
The address bar is a browser-level setting this page cannot reach — change that
one with <code>tron search &lt;engine&gt;</code>, or in
<code>chrome://settings/search</code>.</p>
<label for="searchEngine">New-tab search engine (clearnet)</label>
<select id="searchEngine">
<option value="kagi">Kagi (default)</option>
<option value="ddg">DuckDuckGo (default)</option>
<option value="kagi">Kagi (needs a Kagi subscription)</option>
<option value="neosearch">NeoSearch (private)</option>
<option value="xprivo">Xprivo (private)</option>
<option value="ddg">DuckDuckGo</option>
<option value="altpower">Altpower</option>
<option value="oxiverse">Oxiverse</option>
</select>
Expand Down
Loading
Loading