Config showcase #147
Replies: 17 comments 24 replies
-
|
I use emacs style of keymaps, so far almost works in emacs style. #25 (I'll later link to my glide.ts to catch updates) https://github.com/idlip/d-nix/blob/gol-d/d-setup.org#config I've vibed two functions to show history via glide.commandline: 1. Show all unique history and select to open:glide.keymaps.set("normal", "<C-x><C-l>", async () => {
const allHistory = await browser.history.search({
text: "",
startTime: 0,
maxResults: 1000,
});
const seen = new Set();
const uniqueHistory = [];
for (const item of allHistory) {
if (!seen.has(item.url)) {
seen.add(item.url);
uniqueHistory.push(item);
}
}
glide.commandline.show({
title: "history",
options: uniqueHistory.map((item) => ({
label: item.title || item.url,
render() {
return DOM.create_element("div", {
style: { display: "flex", alignItems: "center", gap: "8px", marginLeft: "1em" },
children: [
DOM.create_element("img", {
src: `page-icon:${item.url}`,
style: { width: "16px", height: "16px" },
}),
DOM.create_element("div", {
children: [
DOM.create_element("div", { children: item.title || item.url }),
DOM.create_element("div", { children: item.url }),
],
}),
],
});
},
async execute() {
const tab = await glide.tabs.get_first({ url: item.url });
if (tab) {
await browser.tabs.update(tab.id, { active: true });
} else {
await browser.tabs.create({ active: true, url: item.url });
}
},
})),
});
}, { description: "Open the history picker" });2. Show all history from current base URL (page) to openasync function getActiveTab() {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
return tab;
}
glide.keymaps.set("normal", "<C-x><C-v>", async () => {
const activeTab = await getActiveTab();
if (!activeTab || !activeTab.url) return;
const origin = (() => {
try { return new URL(activeTab.url).origin; } catch { return null; }
})();
if (!origin) return;
const allHistory = await browser.history.search({
text: "",
startTime: 0,
maxResults: 400,
});
const sameOrigin = allHistory.filter(item => {
try { return new URL(item.url).origin === origin; } catch { return false; }
});
sameOrigin.sort((a, b) => (b.lastVisitTime || 0) - (a.lastVisitTime || 0));
const seen = new Set();
const unique = [];
for (const item of sameOrigin) {
const canonical = item.url.split("#")[0]; // strip fragment for dedupe
if (!seen.has(canonical)) {
seen.add(canonical);
unique.push(item);
}
}
glide.commandline.show({
title: `History for ${origin}`,
options: unique.map(item => ({
label: item.title || item.url,
render() {
return DOM.create_element("div", {
style: { display: "flex", alignItems: "center", gap: "8px", marginLeft: "1em" },
children: [
DOM.create_element("img", {
src: `page-icon:${item.url}`,
style: { width: "16px", height: "16px" },
}),
DOM.create_element("div", {
children: [
DOM.create_element("div", { children: item.title || item.url }),
DOM.create_element("div", { children: item.url }),
],
}),
],
});
},
async execute() {
const existing = await glide.tabs.get_first({ url: item.url });
if (existing) {
await browser.tabs.update(existing.id, { active: true });
} else {
await browser.tabs.create({ active: true, url: item.url });
}
},
})),
});
}, { description: "Show site-specific history of current page" });3. Split window in emacs keybindings (built-in)glide.keymaps.set(
"normal",
"<C-x>2",
async ({ tab_id }) => {
const all_tabs = await glide.tabs.query({});
const current_index = all_tabs.findIndex((t) =>
t.id === tab_id
);
const other = all_tabs[current_index + 1];
if (!other) {
throw new Error("No next tab");
}
glide.unstable.split_views.create([tab_id, other]);
},
{
description:
"Create a split view with the tab to the right",
},
);
glide.keymaps.set(
"normal",
"<C-x>1",
async ({ tab_id }) => {
glide.unstable.split_views.separate(tab_id);
},
{
description: "Close the split view for the current tab",
},
); |
Beta Was this translation helpful? Give feedback.
-
|
I extended the label_generator in https://glide-browser.app/api#glide.o.hint_label_generator to use unique prefixes while still keeping labels as short as possible. function shortest_unique_prefix(needle, haystack) {
let i = 0;
// while all words have the same character at position i, increment i
for (let i in needle) {
const prefix = needle.slice(0, i);
const is_unique = !haystack.some(w => w !== needle && w.startsWith(prefix));
if (is_unique) {
return prefix;
}
}
return needle;
}
function labels(texts) {
// strip numbers, non-ascii text, and annoying-to-type characters
let haystack = texts.map((text) => {
return text.replace(/[0-9]+/g, '').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase();
});
// now shorten as much as we can without losing info
haystack = haystack.map(text => shortest_unique_prefix(text, haystack));
let nums_used = 0;
const result = new Array(haystack.length);
const labelIndices = new Map();
// record the original order so we don't have to keep them in order while calculating.
for (const [index, label] of haystack.entries()) {
if (label === "") {
result[index] = String(nums_used++);
} else {
labelIndices.getOrInsert(label, []).push(index);
}
}
// assign numbers to duplicates and remove them from the haystack.
const unique = [];
for (const [label, indices] of labelIndices) {
// first occurrence gets processed normally, rest get numbers
unique.push({ label, index: indices[0] });
for (let i = 1; i < indices.length; i++) {
result[indices[i]] = String(nums_used++);
}
}
// trim prefixes longer than 3 characters by using letters that occur later in the label.
const used = new Set();
outer: for (const {label, index} of unique) {
if (label.length > 3) {
const base = label.substring(0, 2);
// try each character after the 3rd in turn.
for (const c of label.substring(2)) {
const candidate = base + c;
if (!used.has(candidate)) {
result[index] = candidate;
used.add(candidate);
continue outer;
}
}
// we couldn't shorten it. use a number.
result[index] = String(nums_used++);
} else {
// already short enough, use it as-is.
result[index] = label;
used.add(label);
}
}
// try one last time to shorten prefixes—if we gave up earlier, we might have freed up a spot.
return result.map(text => shortest_unique_prefix(text, result));
}
glide.o.hint_label_generator = async ({ content }) => {
const texts = await content.map((element) => element.textContent);
return labels(texts);
}; |
Beta Was this translation helpful? Give feedback.
-
|
With some free time and custom command line options I've got the start of recreating tridactyl's glide.keymaps.set("normal", "<leader>o", async () => {
// There's probably a better way to keep this strongly typed, but it also doesn't trip any errors right now
let combined = []
const bookmarks = await browser.bookmarks.getRecent(20);
bookmarks.forEach(bmark => combined.push({ title: bmark.title, url: bmark.url }))
combined.push(...bookmarks)
const history = await browser.history.search({ text: "", maxResults: 100 })
history.forEach(entry => combined.push({ title: entry.title, url: entry.url }))
glide.commandline.show({
title: "open",
options: filtered_combined.map((entry) => ({
label: entry.title,
async execute() {
console.log(entry)
const tab = await glide.tabs.get_first({
url: entry.url,
});
if (tab) {
const windowid = tab.windowId;
if (windowid === undefined) {
return
}
await browser.windows.update(windowid, {
focused: true
})
await browser.tabs.update(tab.id, {
active: true,
});
} else {
await browser.tabs.create({
active: true,
url: entry.url,
});
}
},
})),
});
}, { description: "Open the site searcher" });Notaby the architecture differs a little. Glide, so far, doesn't match tridactyl's behavior in 2 main ways for this purpose
I tried porting over trydactyl's implementations, but I'm also a newbie at typescript and the architectural differences meant it wasn't as simple as I would have hoped. That being said grabbing 200 history items and working off of that is aaaalmost there for my use case. I'm excited to see if I can get this working fully well! |
Beta Was this translation helpful? Give feedback.
-
|
Another thing tridactyl can do is search through audio! While this is on the tab search, the lack of inspection on the result means that I had to break this out onto another keybind. I think I might prefer it this way! (There actually might be with the match param, I'll have to look later) If you, like me, suffer with 10 windows and 20 youtube tabs this should help out a little. Selecting an element will focus the tab and window, so you can pause the audio or just figure out where that sound is coming from. glide.keymaps.set("normal", "<leader>A", async () => {
const audible_tabs = await browser.tabs.query({ audible: true })
glide.commandline.show({
title: "open",
options: audible_tabs.map((tab) => ({
label: tab.title,
async execute() {
console.log(tab)
const windowid = tab.windowId
if (windowid === undefined) {
return
}
await browser.windows.update(windowid, { focused: true })
await browser.tabs.update(tab.id, {
active: true,
});
},
})),
});
}, { description: "Search through tabs playing audio" }) |
Beta Was this translation helpful? Give feedback.
-
|
Here is my take on some vanilla vim bindings (vanilla-vim.ts) Tab next/previous with {count}gtglide.keymaps.set("normal", "gT", "tab_prev", { description: "[g]o to previous [T]ab" });
glide.keymaps.set("normal", "gt", "tab_next", { description: "[g]o to next [t]ab" });
for (let i = 1; i < 10; i++) {
glide.keymaps.set("normal", `${i}gt`, `tab ${i - 1}`, { description: `[g]o to [t]ab ${i}` })
glide.keymaps.set("normal", `${i}gT`, `tab ${i * -1}`, { description: `[g]o to [T]ab ${i} from the end` }) // Technically gT should go {count} tabs backwards, but last, second to last, etc is more helpful
};Backspace with ctrl-hglide.keymaps.set(["command", "insert"], "<C-h>", "keys <Backspace>");Alternate buffer with :b#let previousTabId: number | undefined;
browser.tabs.onActivated.addListener((activeInfo) => {
previousTabId = activeInfo.previousTabId;
});
glide.excmds.create({ name: "b#", description: "[b]uffer [#]alternate -> switches to previously active tab" }, async () => {
if (previousTabId) {
await browser.tabs.update(previousTabId, { active: true })
}
});Delete buffer with :bdglide.excmds.create({ name: "bd", description: "[b]uffer [d]elete -> deletes current tab"}, () => {
glide.excmds.execute("tab_close")
});Remove find highlights with :nohglide.excmds.create({ name: "noh", description: "[no] [h]ighlight -> clears find highlights" }, async () => {
await browser.find.removeHighlighting()
});Delete non-active, non-pinned tabs with :tabo[nly]glide.excmds.create({ name: "tabo", description: "[tab] [o]nly -> deletes all non-active non-pinned tabs"}, async () => {
const tabs_to_close = await browser.tabs.query({active: false, pinned: false})
browser.tabs.remove(tabs_to_close.map(t => t.id));
})Search page with / and cycle matches with n/Nglide.keymaps.set("normal", "/", () => glide.findbar.open({mode: "normal", highlight_all: true}));
glide.keymaps.set("normal", "n", () => glide.findbar.next_match());
glide.keymaps.set("normal", "N", () => glide.findbar.previous_match());Undo redo with u and C-rglide.keymaps.set("normal", "u", "undo");
glide.keymaps.set("normal", "<C-r>", "redo");Splits with C-w style bindingsglide.excmds.create({
name: 'vs',
description: '[v]ertical [s]plit => Pick another tab to split view with',
}, async () => {
const activeTab = await glide.tabs.active()
if (activeTab.pinned) return // Can't split pinned tabs at present
const tabs = await glide.tabs.query({ active: false, pinned: false })
glide.commandline.show({
title: "Show other",
options: tabs.map(t => ({
label: t.title,
async execute() {
glide.unstable.split_views.create([activeTab.id, t.id])
}
})),
})
})
glide.keymaps.set('normal', '<C-w>v', 'vs', { description: '[w]indow [v]ertical split => Pick another tab to split view with' })
glide.keymaps.set('normal', '<C-w>c', ({ tab_id }) => {
glide.unstable.split_views.separate(tab_id)
}, { description: '[w]indow [c]lose current split (without closing any tabs)' })
glide.keymaps.set('normal', '<C-w><C-w>', async ({ tab_id }) => {
const split_tabs = await glide.unstable.split_views.get(tab_id)
if (!split_tabs) return
const other_tab = split_tabs.tabs.filter(t => t.id !== tab_id)[0]
await browser.tabs.update(other_tab.id, { active: true })
}, { description: 'oh [w]indow my [w]indow => switch which window in split is focused' })Updates
|
Beta Was this translation helpful? Give feedback.
-
Auto redirectconst redirectRules: Record<string, string> = {
'^https?://(?:www\\.)?youtube\\.com/shorts/([A-Za-z0-9_-]+)': 'https://youtube.com/watch?v=\$1',
};
async function handleBeforeRequest( details: browser.webRequest.OnBeforeRequestDetailsType,
): browser.webRequest.RedirectOption | browser.webRequest.CancelOption { const url = details.url;
for (const [pattern, replacement] of Object.entries(redirectRules)) {
const re = new RegExp(pattern); const match = url.match(re); if (!match) continue;
const newUrl = replacement.replace(/\$(\d+)/g, (full, groupIndexStr) => { const idx = Number(groupIndexStr);
return match[idx] ?? full; }); return { redirectUrl: newUrl }; } return { cancel: false }; }
browser.webRequest.onBeforeRequest.addListener( handleBeforeRequest, { urls: ['<all_urls>'] }, ['blocking'],);Extend context menuasync function ExtendedContextMenu() {
if (!glide.prefs.get("network.protocol-handler.external.mpv")) { glide.prefs.set("network.protocol-handler.external.mpv", true); }
browser.contextMenus.create({ id: "glide-mpv", title: "🎬 Play in mpv", contexts: ["link"],
onclick: (info) => { const mpvUrl = `mpv://#${info.linkUrl}`; browser.tabs.update({ url: mpvUrl }); } })
browser.contextMenus.create({ id: "glide-lens", title: "🔎 Search with Google Lens", contexts: ["image"],
onclick: (info, tab) => { if (!info.srcUrl) return; const encodedUrl = encodeURIComponent(info.srcUrl);
const googleLensUrl = `https://lens.google.com/uploadbyurl?url=${encodedUrl}`; browser.tabs.create({ url: googleLensUrl }); } });
browser.contextMenus.create({ id: "glide-tts", title: "📢 Read with TTS", contexts: ["selection"],
onclick: (info, tab) => { const selection = info.selectionText.trim();
browser.notifications.create("tts", { type: "basic", title: "glide-say-TTS", message: `${selection}`, }); } }); }
glide.autocmds.create("ConfigLoaded", async () => { ExtendedContextMenu(); });
Add custom search engines with search suggestions, fast search selected text p+key
|
Beta Was this translation helpful? Give feedback.
-
mode indicator without barsglide.autocmds.create("ModeChanged", "*", (args) => {
const style_id = "glide-custom-mode-indicator"
const fallback = "--glide-fallback-mode"
// If there is a --current-mode-color var I did not find it but it would make this obsolete :)
const mode_colors: Record<keyof GlideModes, string> = {
"command": "--glide-mode-command",
"hint": "--glide-mode-hint",
"ignore": "--glide-mode-ignore",
"insert": "--glide-mode-insert",
"normal": "--glide-mode-normal",
"op-pending": "--glide-mode-op-pending",
"visual": "--glide-mode-visual",
}
glide.styles.remove(style_id)
glide.styles.add(`
#browser {
border-bottom: 3px solid var(${mode_colors[args.new_mode] ?? fallback})
}
`, { id: style_id })
}) |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
change styling to personal preferences for ALL websites/*
* didn't find any "useful" use cases for this yet but liked
* the idea (saw something similar somewhere once idk)
*/
glide.autocmds.create("UrlEnter", /.*/,
({ tab_id }) => () => glide.content.execute(() => {
const id = "___personal_styles"
const styles_exist = !!document.getElementById(id)
if (styles_exist) return
const css = `
* {
border-radius: 0px !important;
}
`
const style_el = DOM.create_element("style", {
id, children: [css]
})
document.head.appendChild(style_el);
}, { tab_id })
) |
Beta Was this translation helpful? Give feedback.
-
Install the latest versions of your favorite addonsconst addons = [
"ublock-origin",
"darkreader",
"foxyproxy-standard",
"ilyhalight/voice-over-translation",
];
interface Asset { name: string; browser_download_url: string; }
async function getGithubXpi(ownerRepo: string): Promise<string | null> {
const res = await fetch(`https://api.github.com/repos/${ownerRepo}/releases/latest`);
const release = (await res.json()) as { assets?: Asset[] };
const assets = Array.isArray(release.assets) ? release.assets : [];
const firefoxXpi = assets.find(a => a.name.toLowerCase().includes("firefox") &&
a.name.toLowerCase().endsWith(".xpi"),);
if (firefoxXpi) { return firefoxXpi.browser_download_url; }
const anyXpi = assets.find(a => a.name.toLowerCase().endsWith(".xpi"),);
return anyXpi?.browser_download_url ?? null;
}
async function getAmoXpi(addon: string): Promise<string | null> {
const res = await fetch(`https://addons.mozilla.org/api/v5/addons/addon/${addon}/versions/?page_size=1`);
const json = await res.json() as { results? }; const vers = Array.isArray(json.results) ? json.results : [];
return vers[0]?.file?.url
}
async function getXpiUrl(addon: string): Promise<string | null> {
if (addon.includes("/")) { return getGithubXpi(addon); } else { return getAmoXpi(addon); }
}
async function addonsInstall() { for (const addon of addons) {
const xpiUrl = await getXpiUrl(addon); await glide.addons.install(xpiUrl, { force: true });
const icon = "chrome://branding/content/icon32.png";
await browser.notifications.create(addon, {
type: "basic", title: "glide", message: `Addon installed:\n${addon}`, iconUrl: icon, }); }
}
const addons_cmd = glide.excmds.create( { name: "addons_install",
description: "Install necessary browser addons", }, async () => { await addonsInstall(); },);
declare global { interface ExcmdRegistry { addons: typeof addons_cmd; } }if addon name contains |
Beta Was this translation helpful? Give feedback.
-
|
I created a home-manager module to configure glide using Nix I've only been using NixOS for 4-5 months, so this may not the best possible module, but I'm happy with the result and wanted to share it Here's a piece of my configuration file (as an example): { ... }: {
programs.glide-browser = {
enable = true;
settings = {
options.newtab_url = "about:blank";
preferences = {
"general.smoothScroll" = false;
"browser.startup.homepage" = "about:blank";
"browser.newtabpage.enabled" = true;
"browser.urlbar.openintab" = false;
};
keymaps = [
{
modes = "normal";
key = "<leader>cr";
action = "config_reload";
}
{
modes = "normal";
key = "/";
action.__raw = "() => { glide.findbar.open(); }";
}
];
search_engines = [
{
name = "Nix Packages";
keyword = "np";
search_url = "https://search.nixos.org/packages?query={searchTerms}";
favicon_url = "https://search.nixos.org/favicon.png";
}
];
excmds = [
{
info = {
name = "noh";
description = "[no] [h]ighlight -> clears find highlights";
};
fn = ''
async () => {
await browser.find.removeHighlighting();
}
'';
}
];
autocmds = [
{
event = "ModeChanged";
pattern = "insert:*";
callback = ''
async () => {
glide.prefs.set("browser.urlbar.openintab", false);
}
'';
}
];
};
addons = [
{
url = "https://addons.mozilla.org/firefox/downloads/file/4692594/control_panel_for_twitter-4.22.0.xpi";
options.private_browsing_allowed = true;
}
];
styles = [
./styles.css
''
[anonid="glide-commandline-completions"] {
--option-height: 18px;
--glide-header-font-size: 12px;
--glide-cmplt-font-size: 12px;
border: 0;
overflow: hidden;
}
''
];
extraConfig = ''
glide.include("extra.glide.ts");
'';
};
}And the full module:
|
Beta Was this translation helpful? Give feedback.
-
Show QR code with selected text or current URLasync function qrencode() {
async function getSelection() { const tab = await glide.tabs.active(); const [sel] = await browser.tabs.executeScript(tab.id,
{ code: "window.getSelection().toString();", runAt: "document_idle", }); return (sel || "").trim(); };
const selected = await getSelection(); const text = selected || glide.ctx.url;
await glide.content.execute( (qrText: string) => { const popup = document.createElement('div');
popup.style.cssText = ` position: fixed; z-index: 10000; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.75); display: flex; justify-content: center; align-items: center; cursor: pointer;`;
popup.addEventListener('click', (e) => { if (e.target === popup) { document.body.removeChild(popup); } });
const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' || e.key === ' ') { document.body.removeChild(popup);
window.removeEventListener('keydown', onKeyDown); } }; window.addEventListener('keydown', onKeyDown);
(async () => {
const res = await fetch( `https://api.qrserver.com/v1/create-qr-code/?size=1000x1000&data=${encodeURIComponent( qrText)}`);
const blob = await res.blob(); const imageUrl = URL.createObjectURL(blob);
const img = document.createElement('img'); img.src = imageUrl; img.alt = 'QR Code';
img.style.maxWidth = '90%'; img.style.maxHeight = '90%';
popup.appendChild(img); document.body.appendChild(popup);
})(); }, { tab_id: await glide.tabs.active(), args: [text], }); }
const qr_cmd = glide.excmds.create( { name: 'qr', description: 'QR encode selected or current URL', },
async () => { await qrencode(); });
declare global { interface ExcmdRegistry { tabs_toggle: typeof qr_cmd; } } |
Beta Was this translation helpful? Give feedback.
-
Hide annoying pop-upsconst popup_toggle_cmd = glide.excmds.create({ name: "popup_toggle",
description: "Toggle visibility of fixed/sticky elements on the page" },
async ({ tab_id }) => { await glide.content.execute(() => {
const ATTR = "data-glide-hidden-fixed";
const elements = document.querySelectorAll("*");
const alreadyHidden = document.querySelector(`[${ATTR}]`);
elements.forEach((el) => {
const htmlEl = el as HTMLElement;
const style = window.getComputedStyle(htmlEl);
const position = style.getPropertyValue("position");
if (position === "fixed" || position === "sticky") {
if (alreadyHidden) {
htmlEl.style.removeProperty("display");
htmlEl.removeAttribute(ATTR);
} else {
htmlEl.style.setProperty("display", "none", "important");
htmlEl.setAttribute(ATTR, "true");
} } }); }, { tab_id }); });
declare global { interface ExcmdRegistry { popup_toggle: typeof popup_toggle_cmd }}; |
Beta Was this translation helpful? Give feedback.
-
|
This is a feature from Emacs' avy where you can first search, then display hints for the match results. To use, search for something with Control+f, then while the search-bar is open, press Control+d to display hints for the matches. Warning: vibecoded. Switch to hinting from search matches
// ---------------------------------------------------------------------------
// Helper: read the current find match from the content process selection
// ---------------------------------------------------------------------------
async function getQueryFromSelection(tab_id: number): Promise<string> {
try {
return await glide.content.execute(() => {
return window.getSelection()?.toString() ?? "";
}, { tab_id });
} catch (_) {
return "";
}
}
// ---------------------------------------------------------------------------
// C-d → avy: mark matching elements, then hint only those
// ---------------------------------------------------------------------------
const AVY_ATTR = "data-glide-avy-match";
async function avyFromFindbar(tab_id: number) {
if (!glide.findbar.is_open()) return;
const query = await getQueryFromSelection(tab_id);
console.log("[isearch-avy] query from selection:", JSON.stringify(query));
if (!query) return;
// Close findbar so hints are unobstructed
await glide.findbar.close();
await glide.excmds.execute("mode_change normal");
const searchQuery = query;
// Step 1: In the content process, walk the DOM and mark the *smallest*
// elements whose text contains the query with a data attribute.
// This gives the hint system real elements to attach to.
const matchCount = await glide.content.execute(
function markMatches(query: string, attr: string) {
const q = query.toLowerCase();
// Clean up any previous marks
document.querySelectorAll(`[${attr}]`).forEach((el) =>
el.removeAttribute(attr)
);
let count = 0;
// TreeWalker to find text nodes containing the query
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
if ((node.textContent ?? "").toLowerCase().includes(q)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
},
}
);
const parents = new Set<Element>();
while (walker.nextNode()) {
const textNode = walker.currentNode;
const parent = textNode.parentElement;
if (parent && !parents.has(parent)) {
// Only mark leaf-ish elements: skip if a child element also matches
// (we want the most specific/smallest element)
const childElements = parent.children;
let childMatches = false;
for (let i = 0; i < childElements.length; i++) {
if (
(childElements[i]!.textContent ?? "")
.toLowerCase()
.includes(q) &&
childElements[i] !== parent
) {
// A child element also contains the match — we'll mark that
// child instead when the walker reaches its text node
childMatches = true;
break;
}
}
// Actually, the walker will visit child text nodes too, so just
// mark every parent of a matching text node. The set deduplicates.
parents.add(parent);
}
}
// Now filter to keep only the most specific (deepest) elements.
// Remove any element from the set if one of its descendants is also in it.
const toMark = [...parents].filter((el) => {
for (const other of parents) {
if (other !== el && el.contains(other)) {
return false; // el is an ancestor of another match, skip it
}
}
return true;
});
for (const el of toMark) {
el.setAttribute(attr, "true");
count++;
}
return count;
},
{ tab_id, args: [searchQuery, AVY_ATTR] }
);
console.log("[isearch-avy] marked", matchCount, "elements for avy");
if (matchCount === 0) return;
// Step 2: Show hints targeting only the marked elements
await glide.excmds.execute(`hint -s "[${AVY_ATTR}]"`);
}
glide.keymaps.set("normal", "<C-d>", async ({ tab_id }) => {
if (glide.findbar.is_open()) {
await avyFromFindbar(tab_id);
} else {
glide.hints.show();
}
}, { description: "isearch: avy jump / hints" });
glide.keymaps.set("insert", "<C-d>", async ({ tab_id }) => {
if (glide.findbar.is_focused() || glide.findbar.is_open()) {
await avyFromFindbar(tab_id);
} else {
glide.hints.show();
}
}, { description: "isearch: avy jump / hints" }); |
Beta Was this translation helpful? Give feedback.
-
|
Features
Source Code// Gnarly Tab Edit: edit tabs in a text editor
// Features:
// - Opens current tabs in the configured text editor
// - Deleting a line => Deletes the tab
// - Reordering lines => Reorders tabs
// - Adding a line (with "url": "...") => Creates new tab
// - Setting active true => Focuses the tab
// - Setting pinned to true/false => pins/unpins the tab
glide.excmds.create({ name: "tab_edit", description: "Edit tabs in a text editor" }, async () => {
const tabs = await get_list_of_current_tabs()
const tempfile = await save_tabs_to_temp_file(tabs)
await let_user_edit_file_and_wait_for_exit(tempfile)
const updated_tabs = await get_list_of_tabs_from_file(tempfile)
await close_unwanted_tabs(tabs, updated_tabs)
await update_tab_url(tabs, updated_tabs)
await update_tab_pinned_ness(tabs, updated_tabs)
await update_focused_tab(tabs, updated_tabs)
await open_new_tabs_and_adjust_order(updated_tabs)
});
async function get_list_of_current_tabs() {
return await browser.tabs.query({})
}
async function save_tabs_to_temp_file(tabs) {
const tab_lines = tabs
.toSorted((a, b) => a.index - b.index)
.map(stringify);
const tempfile = await mktemp("glide_tab_edit.XXXXXX")
await glide.fs.write(tempfile, tab_lines.join("\n"));
return tempfile
}
async function let_user_edit_file_and_wait_for_exit(tempfile) {
const edit_cmd = await glide.process.execute("gnome-text-editor", [
"--standalone",
tempfile,
]);
const edit_result = await edit_cmd.wait();
if (edit_result.exit_code !== 0) {
throw new Error(`Editor command failed with exit code ${edit_result.exit_code}`);
}
}
async function get_list_of_tabs_from_file(tempfile) {
const edited_content = await glide.fs.read(tempfile, "utf8")
const tabs_to_keep = edited_content
.split("\n")
.filter((line) => line.trim().length > 0)
.filter((line) => !line.startsWith("//"))
.map((line) => JSON.parse(line))
return tabs_to_keep
}
async function close_unwanted_tabs(current_tabs, updated_tabs) {
const tabs_to_keep = updated_tabs.map(t => t.id)
const tab_ids_to_close = current_tabs
.filter(hasId)
.filter(t => !tabs_to_keep.includes(t.id))
.map(t => t.id)
await browser.tabs.remove(tab_ids_to_close)
}
async function update_tab_url(current_tabs, updated_tabs) {
updated_tabs
.filter(hasId)
.filter(ut => find(current_tabs, ut.id).url != ut.url)
.forEach(async t => await browser.tabs.update(t.id, {url: t.url}))
}
async function update_tab_pinned_ness(current_tabs, updated_tabs) {
updated_tabs
.filter(hasId)
.filter(ut => find(current_tabs, ut.id).pinned != ut.pinned)
.forEach(async t => await browser.tabs.update(t.id, {pinned: t.pinned}))
}
async function update_focused_tab(current_tabs, updated_tabs) {
updated_tabs
.filter(hasId)
.filter(t => t.active)
.filter(ut => find(current_tabs, ut.id).active != ut.active)
.forEach(async t => await browser.tabs.update(t.id, {active: t.active}))
}
async function open_new_tabs_and_adjust_order(updated_tabs) {
updated_tabs
.forEach(async (t, i) => {
if (t.id === undefined) {
await browser.tabs.create({
url: t.url,
index: i,
active: t.active,
pinned: t.pinned
})
} else {
await browser.tabs.move(t.id, {index: i})
}
})
}
async function mktemp(template) {
const mktemp_cmd = await glide.process.execute("mktemp", ["-t", template, "--suffix", ".json"]);
return (await mktemp_cmd.stdout.text()).trim();
}
function hasId(tab) {
return tab?.id !== undefined
}
function find(list, id) {
return list.find(i => i.id === id)
}
function stringify(tab) {
const id = JSON.stringify(tab.id).padStart(4, " ")
const title = JSON.stringify(tab.title?.replace(/\n/g, " ").substring(0,40)).padEnd(42, " ")
const active = JSON.stringify(tab.active).padStart(5, " ")
const pinned = JSON.stringify(tab.pinned).padStart(5, " ")
const url = JSON.stringify(tab.url)
return `{"id":${id},"title":${title},"active":${active},"pinned":${pinned},"url":${url}}`
} |
Beta Was this translation helpful? Give feedback.
-
|
Updated my custom bottom bar to include Glide’s current mode (uses the same mode colors as the UI) and the current key sequence. update-bottom-status-bar.movSnippet
const status_bar_id = 'glide-status-bar';
const STATUS_TAB_TITLE_MAX = 28;
const STATUS_TAB_CLOSE_RESYNC_MS = 32;
const mode_colors: Record<keyof GlideModes, string> = {
command: '--glide-mode-command',
hint: '--glide-mode-hint',
ignore: '--glide-mode-ignore',
insert: '--glide-mode-insert',
normal: '--glide-mode-normal',
'op-pending': '--glide-mode-op-pending',
visual: '--glide-mode-visual',
};
const fallback_mode_color = '--glide-fallback-mode';
function truncate_status_label(text: string, max = STATUS_TAB_TITLE_MAX): string {
if (text.length <= max) return text;
return `${text.slice(0, Math.max(0, max - 1))}…`;
}
function create_status_bar_shell(): HTMLElement {
return DOM.create_element('div', {
id: status_bar_id,
children: [
DOM.create_element('div', {
className: 'glide-status-tabs',
attributes: { role: 'tablist', 'aria-label': 'Window tabs' },
children: [],
}),
DOM.create_element('div', {
className: 'glide-status-right',
children: [],
}),
],
}) as HTMLElement;
}
function repair_status_bar_children(status_bar: HTMLElement) {
let tabs = status_bar.querySelector('.glide-status-tabs');
let right = status_bar.querySelector('.glide-status-right');
if (!tabs || !right) {
status_bar.replaceChildren(
DOM.create_element('div', {
className: 'glide-status-tabs',
attributes: { role: 'tablist', 'aria-label': 'Window tabs' },
children: [],
}),
DOM.create_element('div', { className: 'glide-status-right', children: [] }),
);
}
}
function ensure_status_bar(): HTMLElement {
let status_bar = document.getElementById(status_bar_id) as HTMLElement | null;
if (!status_bar) {
status_bar = create_status_bar_shell();
document.getElementById('browser')?.appendChild(status_bar);
} else {
repair_status_bar_children(status_bar);
}
return status_bar;
}
let status_bar_refresh_timer: ReturnType<typeof setTimeout> | undefined;
function schedule_update_status_bar(debounce_ms = 0) {
if (status_bar_refresh_timer !== undefined) {
clearTimeout(status_bar_refresh_timer);
}
status_bar_refresh_timer = setTimeout(() => {
status_bar_refresh_timer = undefined;
update_status_bar();
}, debounce_ms);
}
function refresh_status_bar_after_tab_removed() {
update_status_bar();
queueMicrotask(() => update_status_bar());
requestAnimationFrame(() => {
update_status_bar();
requestAnimationFrame(() => update_status_bar());
});
setTimeout(() => update_status_bar(), STATUS_TAB_CLOSE_RESYNC_MS);
setTimeout(() => update_status_bar(), 120);
setTimeout(() => update_status_bar(), 280);
}
let status_bar_update_generation = 0;
let status_bar_key_sequence: string[] = [];
let status_bar_key_partial = false;
function status_bar_sep(): Text {
return document.createTextNode(' · ');
}
async function query_tabs_for_status_bar(
window_id?: number | undefined,
): Promise<Browser.Tabs.Tab[]> {
if (window_id != null) {
return browser.tabs.query({ windowId: window_id });
}
try {
const w = await browser.windows.getCurrent();
if (w.id != null) {
return browser.tabs.query({ windowId: w.id });
}
} catch {}
return browser.tabs.query({ currentWindow: true });
}
async function resolve_active_tab_id(window_id?: number | undefined): Promise<number | undefined> {
try {
return (await glide.tabs.active()).id;
} catch {
const q =
window_id != null
? { active: true, windowId: window_id }
: { active: true, currentWindow: true };
const [tab] = await browser.tabs.query(q);
return tab?.id;
}
}
function render_status_bar_right_panel(right: HTMLElement, active_tab_id: number | undefined) {
const mode_var = mode_colors[glide.ctx.mode] ?? fallback_mode_color;
const id_label = active_tab_id != null ? String(active_tab_id) : '—';
const children: Node[] = [
document.createTextNode('Tab ID: '),
DOM.create_element('span', { className: 'glide-status-tab-id', textContent: id_label }),
];
children.push(status_bar_sep());
if (status_bar_key_sequence.length > 0) {
const keys_label = status_bar_key_sequence.join(' ');
children.push(
DOM.create_element('span', {
className: 'glide-status-keys',
attributes: {
'aria-label': `${status_bar_key_partial ? 'Partial' : 'Complete'} mapping: ${keys_label}`,
'aria-live': 'polite',
...(status_bar_key_partial ? { 'data-partial': 'true' } : {}),
},
textContent: keys_label,
}),
);
children.push(status_bar_sep());
}
children.push(
DOM.create_element('span', {
className: 'glide-status-mode',
textContent: glide.ctx.mode,
style: { color: `var(${mode_var})` },
}),
);
right.replaceChildren(...children);
}
async function update_status_bar(retry_if_missing_children = true) {
const generation = ++status_bar_update_generation;
const is_current = () => generation === status_bar_update_generation;
const status_bar = ensure_status_bar();
let tabs_container = status_bar.querySelector('.glide-status-tabs');
let right = status_bar.querySelector('.glide-status-right');
if (!tabs_container || !right) {
ensure_status_bar();
if (retry_if_missing_children) {
return update_status_bar(false);
}
return;
}
try {
const win = await browser.windows.getCurrent().catch(() => undefined);
const window_id = win?.id;
const raw_tabs = await query_tabs_for_status_bar(window_id);
if (!is_current()) return;
const all_tabs = raw_tabs
.filter((t): t is Browser.Tabs.Tab & { id: number } => t.id != null)
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
const active_tab_id = await resolve_active_tab_id(window_id);
if (!is_current()) return;
tabs_container = status_bar.querySelector('.glide-status-tabs');
right = status_bar.querySelector('.glide-status-right');
if (!tabs_container || !right) return;
tabs_container.innerHTML = '';
if (!is_current()) return;
if (all_tabs.length === 0) {
if (!is_current()) return;
tabs_container.textContent = 'No tabs';
render_status_bar_right_panel(right as HTMLElement, active_tab_id);
return;
}
for (let i = 0; i < all_tabs.length; i++) {
if (!is_current()) return;
const tab = all_tabs[i];
if (!tab) continue;
const is_active = tab.id === active_tab_id;
const full_title = tab.title || 'Untitled';
const tab_title = truncate_status_label(full_title);
const tab_element = DOM.create_element('span', {
className: is_active ? 'glide-tab-item active' : 'glide-tab-item',
attributes: {
role: 'tab',
'aria-selected': is_active ? 'true' : 'false',
title: full_title,
},
textContent: `${i + 1}: ${tab_title}`,
}) as HTMLElement;
tab_element.setAttribute('data-tab-id', String(tab.id));
const tab_id = tab.id;
tab_element.addEventListener('click', async () => {
try {
await browser.tabs.update(tab_id, { active: true });
} catch (e) {
console.error('Failed to switch tab:', e);
}
});
tabs_container.appendChild(tab_element);
}
if (!is_current()) return;
render_status_bar_right_panel(right as HTMLElement, active_tab_id);
} catch (e) {
if (!is_current()) return;
console.error('update_status_bar:', e);
const tc = status_bar.querySelector('.glide-status-tabs');
const r = status_bar.querySelector('.glide-status-right') as HTMLElement | null;
if (tc) tc.textContent = 'No tabs';
if (r) r.textContent = 'Error loading tabs';
}
}
glide.autocmds.create('ConfigLoaded', async () => {
status_bar_key_sequence = [];
status_bar_key_partial = false;
document.getElementById(status_bar_id)?.remove();
const status_bar = create_status_bar_shell();
document.getElementById('browser')?.appendChild(status_bar);
schedule_update_status_bar(0);
});
glide.autocmds.create('WindowLoaded', async () => {
ensure_status_bar();
await update_status_bar();
});
glide.autocmds.create('ModeChanged', '*', (args) => {
status_bar_key_sequence = [];
status_bar_key_partial = false;
const style_id = 'glide-custom-mode-indicator';
glide.styles.remove(style_id);
glide.styles.add(
`
#browser {
border-bottom: 3px solid var(${mode_colors[args.new_mode] ?? fallback_mode_color});
}
`,
{ id: style_id },
);
schedule_update_status_bar(0);
});
glide.autocmds.create('KeyStateChanged', (args) => {
status_bar_key_sequence = [...args.sequence];
status_bar_key_partial = args.partial;
schedule_update_status_bar(0);
});
glide.autocmds.create('UrlEnter', /.*/, async () => {
schedule_update_status_bar(0);
});
browser.tabs.onActivated.addListener(() => {
schedule_update_status_bar(0);
});
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.url || changeInfo.title) {
schedule_update_status_bar(0);
}
});
browser.tabs.onCreated.addListener(() => {
schedule_update_status_bar(0);
});
browser.tabs.onMoved.addListener(() => {
schedule_update_status_bar(0);
});
browser.tabs.onRemoved.addListener(() => {
refresh_status_bar_after_tab_removed();
});
glide.styles.add(
`
#${status_bar_id} {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 26px;
background-color: #1a1a1a;
color: #c8c8c8;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
font-size: 11px;
line-height: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
border-top: 1px solid #333333;
z-index: 10000;
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
#${status_bar_id} .glide-status-tabs {
display: flex;
align-items: center;
gap: 2px 8px;
flex: 1;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
#${status_bar_id} .glide-status-tabs::-webkit-scrollbar {
display: none;
}
#${status_bar_id} .glide-tab-item {
flex-shrink: 0;
color: #8a8a8a;
cursor: pointer;
padding: 3px 6px;
border-radius: 3px;
white-space: nowrap;
transition: background-color 0.15s ease, color 0.15s ease;
}
#${status_bar_id} .glide-tab-item:hover {
background-color: #2c2c2c;
color: #e0e0e0;
}
#${status_bar_id} .glide-tab-item:focus-visible {
outline: 1px solid #569cd6;
outline-offset: 1px;
}
#${status_bar_id} .glide-tab-item.active {
color: #569cd6;
font-weight: 600;
background-color: #252525;
}
#${status_bar_id} .glide-status-right {
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
gap: 0;
color: #7a7a7a;
margin-left: 10px;
white-space: nowrap;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
#${status_bar_id} .glide-status-tab-id {
color: #a8a8a8;
font-weight: 600;
}
#${status_bar_id} .glide-status-keys {
max-width: 42vw;
overflow: hidden;
flex-shrink: 1;
min-width: 0;
text-overflow: ellipsis;
color: #929292;
font-weight: 500;
letter-spacing: 0.06em;
white-space: nowrap;
}
#${status_bar_id} .glide-status-keys[data-partial="true"] {
color: #a8a8a8;
}
#${status_bar_id} .glide-status-mode {
font-weight: 600;
letter-spacing: 0.02em;
}
#browser {
padding-bottom: 26px;
}
`,
{ id: 'glide-status-bar-styles' },
); |
Beta Was this translation helpful? Give feedback.
-
Take full-page screenshotconst screenshot_cmd = glide.excmds.create(
{
name: "screenshot",
description: "Take a full-page screenshot",
},
async ({ tab_id }) => {
const dims = await glide.content.execute(
() => ({
width: Math.max(
document.documentElement.scrollWidth,
document.body.scrollWidth,
),
height: Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight,
),
}),
{ tab_id },
);
const dataUrl = await browser.tabs.captureTab(tab_id, {
format: "png",
rect: { x: 0, y: 0, width: dims.width, height: dims.height },
});
await browser.tabs.create({ url: dataUrl });
},
);
declare global { interface ExcmdRegistry { screenshot: typeof screenshot_cmd; }} |
Beta Was this translation helpful? Give feedback.







Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Use this discussion to share any glide config you've written that you think is cool!
Beta Was this translation helpful? Give feedback.
All reactions