+
-
-
-
-
+
+
+
+
+
-
-
@@ -248,6 +449,9 @@ const normalize = (raw) => {
};
const input = document.getElementById('url-input');
+const tabStrip = document.getElementById('tabs');
+const siteIcon = document.getElementById('site-icon');
+let currentUrl = '';
const isEditableTarget = (target) => {
if (!target || !(target instanceof Element)) return false;
@@ -262,70 +466,164 @@ document.getElementById('reload').addEventListener('click', () => post('reload')
document.getElementById('props-btn').addEventListener('click', () => post('open_properties'));
document.getElementById('adblock-btn').addEventListener('click', () => post('toggle_adblock'));
document.getElementById('downloads-btn').addEventListener('click', () => post('toggle_downloads'));
+document.getElementById('add-btn').addEventListener('click', () => post('new_tab'));
document.getElementById('popup-yes').addEventListener('click', () => post('popup_allow_yes'));
document.getElementById('popup-no').addEventListener('click', () => post('popup_allow_no'));
+input.addEventListener('focus', () => input.select());
+
input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
const next = normalize(input.value);
- if (next) post('navigate:' + next);
+ if (next) {
+ post('navigate:' + next);
+ input.blur();
+ }
+ return;
+ }
+ if (ev.key === 'Escape') {
+ ev.preventDefault();
+ input.value = currentUrl;
+ input.blur();
}
});
+// Plain mouse wheel scrolls the tab strip horizontally.
+tabStrip.addEventListener('wheel', (ev) => {
+ if (ev.deltaY === 0) return;
+ ev.preventDefault();
+ tabStrip.scrollLeft += ev.deltaY;
+}, { passive: false });
+
+const setSiteIcon = (url) => {
+ let state = 'search';
+ let hint = 'Search or enter address';
+ try {
+ const parsed = new URL(url);
+ if (parsed.protocol === 'https:') { state = 'secure'; hint = 'Connection is secure (HTTPS)'; }
+ else if (parsed.protocol === 'http:') { state = 'insecure'; hint = 'Not secure (HTTP)'; }
+ else if (parsed.protocol === 'data:' || parsed.protocol === 'file:' || parsed.protocol === 'about:') {
+ state = 'internal'; hint = 'RustyWolf internal page';
+ }
+ } catch (_) {}
+ siteIcon.dataset.state = state;
+ siteIcon.title = hint;
+};
+
+const hostOf = (url) => {
+ try { return new URL(url).hostname.replace(/^www\./, ''); } catch (_) { return ''; }
+};
+
+const hashHue = (text) => {
+ let hash = 0;
+ for (let i = 0; i < text.length; i += 1) hash = (hash * 31 + text.charCodeAt(i)) % 360;
+ return hash;
+};
+
+const ICON_PIN = '
';
+const ICON_CLOSE = '
';
+const ICON_MOON = '
';
+
const shortTitle = (tab, i) => {
- if (tab.title && tab.title.trim()) return tab.title.trim().slice(0, 22);
- try { return new URL(tab.url).hostname || 'Tab ' + (i + 1); }
- catch (_) { return 'Tab ' + (i + 1); }
+ if (tab.title && tab.title.trim()) return tab.title.trim();
+ const host = hostOf(tab.url);
+ return host || 'Tab ' + (i + 1);
+};
+
+const makeTab = (tab, i, activeId) => {
+ const isActive = tab.id === activeId;
+ const el = document.createElement('div');
+ el.className = 'tab' + (isActive ? ' active' : '') + (tab.loaded ? '' : ' sleeping');
+ el.title = tab.url;
+ el.setAttribute('role', 'tab');
+ el.setAttribute('aria-selected', isActive ? 'true' : 'false');
+ el.tabIndex = 0;
+ el.addEventListener('click', () => post('switch_tab:' + tab.id));
+ el.addEventListener('keydown', (ev) => {
+ if (ev.target !== el) return;
+ if (ev.key === 'Enter' || ev.key === ' ') {
+ ev.preventDefault();
+ post('switch_tab:' + tab.id);
+ }
+ });
+ el.addEventListener('auxclick', (ev) => {
+ if (ev.button === 1) {
+ ev.preventDefault();
+ post('close_tab:' + tab.id);
+ }
+ });
+
+ const host = hostOf(tab.url);
+ const favicon = document.createElement('span');
+ favicon.className = 'tab-favicon';
+ if (host) {
+ const hue = hashHue(host);
+ favicon.textContent = host.charAt(0).toUpperCase();
+ favicon.style.background = 'linear-gradient(135deg, hsl(' + hue + ', 62%, 50%), hsl(' + hue + ', 68%, 36%))';
+ favicon.title = host;
+ } else if ((tab.url || '').startsWith('data:')) {
+ favicon.textContent = 'R';
+ favicon.style.background = 'linear-gradient(135deg, hsl(218, 62%, 52%), hsl(218, 68%, 38%))';
+ favicon.title = 'RustyWolf internal page';
+ } else {
+ favicon.textContent = '?';
+ favicon.style.background = 'linear-gradient(135deg, hsl(220, 10%, 38%), hsl(220, 12%, 28%))';
+ favicon.title = 'New tab';
+ }
+ el.appendChild(favicon);
+
+ const title = document.createElement('span');
+ title.className = 'tab-title';
+ title.textContent = shortTitle(tab, i);
+ el.appendChild(title);
+
+ if (!tab.loaded) {
+ const sleep = document.createElement('span');
+ sleep.className = 'tab-sleep';
+ sleep.title = 'Suspended to save memory - click the tab to reload it';
+ sleep.innerHTML = ICON_MOON;
+ el.appendChild(sleep);
+ }
+
+ const pin = document.createElement('button');
+ pin.className = 'tab-pin' + (tab.kirEnabled ? ' on' : '');
+ pin.title = tab.kirEnabled
+ ? 'Keeping in RAM (KIR): on - click to let it suspend when inactive'
+ : 'Keeping in RAM (KIR): off - click to keep this tab loaded in the background';
+ pin.setAttribute('aria-label', 'Toggle keeping in RAM');
+ pin.setAttribute('aria-pressed', tab.kirEnabled ? 'true' : 'false');
+ pin.innerHTML = ICON_PIN;
+ pin.addEventListener('click', (ev) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+ post('toggle_kir:' + tab.id);
+ });
+ el.appendChild(pin);
+
+ const close = document.createElement('button');
+ close.className = 'tab-close';
+ close.title = 'Close tab (Ctrl+W)';
+ close.setAttribute('aria-label', 'Close tab');
+ close.innerHTML = ICON_CLOSE;
+ close.addEventListener('click', (ev) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+ post('close_tab:' + tab.id);
+ });
+ el.appendChild(close);
+
+ return el;
};
window.__rustywolfSetTabs = (state) => {
- const row = document.getElementById('tabs-row');
- row.innerHTML = '';
- state.tabs.forEach((tab, i) => {
- const btn = document.createElement('button');
- btn.className =
- 'tab-btn' +
- (tab.id === state.activeId ? ' active' : '') +
- (tab.loaded ? '' : ' unloaded');
- btn.title = tab.url;
- btn.addEventListener('click', () => post('switch_tab:' + tab.id));
+ tabStrip.innerHTML = '';
+ state.tabs.forEach((tab, i) => tabStrip.appendChild(makeTab(tab, i, state.activeId)));
- const title = document.createElement('span');
- title.className = 'tab-title';
- title.textContent = shortTitle(tab, i) + (tab.loaded ? '' : ' [S]');
- btn.appendChild(title);
-
- const kir = document.createElement('button');
- kir.className = 'kir-btn' + (tab.kirEnabled ? ' active' : '');
- kir.textContent = 'K';
- kir.title = tab.kirEnabled ? 'KIR On' : 'KIR Off';
- kir.addEventListener('click', (ev) => {
- ev.stopPropagation();
- ev.preventDefault();
- post('toggle_kir:' + tab.id);
- });
- btn.appendChild(kir);
-
- const x = document.createElement('button');
- x.className = 'close-btn';
- x.textContent = '-'; x.title = 'Close tab';
- x.addEventListener('click', (ev) => {
- ev.stopPropagation();
- ev.preventDefault();
- post('close_tab:' + tab.id);
- });
- btn.appendChild(x);
- row.appendChild(btn);
- });
-
- const add = document.createElement('button');
- add.id = 'add-btn'; add.textContent = '+'; add.title = 'New tab';
- add.addEventListener('click', () => post('new_tab'));
- row.appendChild(add);
-
- const active = state.tabs.find(t => t.id === state.activeId);
- if (active) input.value = (active.url && active.url !== 'about:blank') ? active.url : '';
+ const active = state.tabs.find((t) => t.id === state.activeId);
+ currentUrl = (active && active.url && active.url !== 'about:blank') ? active.url : '';
+ setSiteIcon(currentUrl);
+ if (document.activeElement !== input) input.value = currentUrl;
};
const shortPath = (value) => {
@@ -340,18 +638,23 @@ window.__rustywolfSetDownloads = (state) => {
const items = (state && Array.isArray(state.downloads)) ? state.downloads : [];
const panelOpen = !!(state && state.panelOpen);
row.innerHTML = '';
+ button.classList.toggle('active', panelOpen);
+ button.setAttribute('aria-pressed', panelOpen ? 'true' : 'false');
if (!panelOpen) {
row.style.display = 'none';
- button.classList.remove('active');
return;
}
- button.classList.add('active');
row.style.display = 'flex';
if (items.length === 0) {
const empty = document.createElement('div');
empty.className = 'dl-empty';
- empty.textContent = 'No active downloads';
+ const glyph = document.createElement('span');
+ glyph.innerHTML = '
';
+ const label = document.createElement('span');
+ label.textContent = 'No active downloads';
+ empty.appendChild(glyph);
+ empty.appendChild(label);
row.appendChild(empty);
return;
}
@@ -424,13 +727,11 @@ window.__rustywolfSetDownloads = (state) => {
window.__rustywolfSetAdblock = (enabled) => {
const button = document.getElementById('adblock-btn');
if (!button) return;
- if (enabled) {
- button.classList.add('active');
- button.title = 'Adblock: On';
- } else {
- button.classList.remove('active');
- button.title = 'Adblock: Off';
- }
+ button.classList.toggle('active', !!enabled);
+ button.setAttribute('aria-pressed', enabled ? 'true' : 'false');
+ button.title = enabled
+ ? 'Ad blocking: on - click to turn off'
+ : 'Ad blocking: off - click to turn on';
};
window.__rustywolfSetPopupPrompt = (state) => {
@@ -2000,16 +2301,22 @@ fn render_properties_html(state: &BrowserState) -> String {
format!(
"
RustyWolf Properties\
- \
-
Browser Properties
Session history and download activity.
\
-
Browsing History
{}
| Visited | Title | URL |
{history_rows}
\
-
Download History
{}
| Updated | Status | Saved To | Source URL |
{download_rows}
\
- ",
+ \
+
Browser Properties
Session browsing history and download activity.
\
+
Browsing History
{}
| Visited | Title | URL |
{history_rows}
\
+
Download History
{}
| Updated | Status | Saved To | Source URL |
{download_rows}
\
+
",
if history_is_truncated {
format!("Showing latest {PROPERTIES_MAX_ROWS} entries.")
} else {