dotfiles/tools/browser/surfingkeys.js
2025-02-23 19:52:23 +00:00

742 lines
21 KiB
JavaScript

// Paste this into surfingkeys advanced settings
// or use:
// Load settings from: https://raw.githubusercontent.com/js0ny/dotfiles/refs/heads/master/tools/browser/surfingkeys.js
// Browse to Extension > Surfingkeys > Allow access to file URLs to enable local file access
// Windows: file:///C:/Users/username/.dotfiles/tools/browser/surfingkeys.js
// Linux: file:///home/username/.dotfiles/tools/browser/surfingkeys.js
// macOS: file:///Users/username/.dotfiles/tools/browser/surfingkeys.js
// #region Example
/** Examples
// an example to create a new mapping `ctrl-y`
api.mapkey('<ctrl-y>', 'Show me the money', function() {
Front.showPopup('a well-known phrase uttered by characters in the 1996 film Jerry Maguire (Escape to close).');
});
// an example to replace `T` with `gt`, click `Default mappings` to see how `T` works.
api.map('gt', 'T');
// an example to remove mapkey `Ctrl-i`
api.unmap('<ctrl-i>');
*/
// #endregion
// #region Settings
settings.language = "zh-CN";
settings.showModeStatus = false;
// #endregion
// #region Helper
// import the API so that no need to use `api` prefix
const {
aceVimMap,
addVimMapKey,
mapkey,
imap,
imapkey,
getClickableElements,
vmapkey,
map,
unmap,
cmap,
addSearchAlias,
removeSearchAlias,
tabOpenLink,
readText,
Clipboard,
Front,
Hints,
Visual,
RUNTIME,
} = api;
// Keymap, reference https://github.com/texiwustion/colemak_config_for_surfingkeys/tree/main
const forward = {
add: function (key) {
// 转发即将被 unmap 的键
return api.map(`for${key}`, key);
},
cancel: function (key) {
// 删除转发生成的键
api.unmap(`for${key}`);
api.unmap(key);
},
use: function (key) {
return `for${key}`;
},
};
const colemak = {
forward: function (key) {
// 转发即将被 unmap 的键
api.map(key, `col${key}`);
api.unmap(`col${key}`);
},
use: function (key) {
return `col${key}`;
},
map: function (a, b) {
api.map(colemak.use(a), forward.use(b));
},
};
const vForward = {
add: function (key) {
// 转发即将被 unmap 的键
return api.vmap(`vfor${key}`, key);
},
cancel: function (key) {
// 删除转发生成的键
api.vunmap(`vfor${key}`);
api.vunmap(key);
},
use: function (key) {
return `vfor${key}`;
},
};
const vColemak = {
forward: function (key) {
// 转发即将被 unmap 的键
api.vmap(key, `vcol${key}`);
api.vunmap(`vcol${key}`);
},
use: function (key) {
return `vcol${key}`;
},
map: function (a, b) {
api.vmap(vColemak.use(a), vForward.use(b));
},
};
const forwardFactory = {
push: function (mapLists) {
// forward original keys
for (const key in mapLists) { // `const` better than `let`
forward.add(mapLists[key]);
}
},
map: function (mapLists) {
for (const key in mapLists) {
colemak.map(key, mapLists[key]);
}
},
pull: function (mapLists) {
for (const key in mapLists) {
forward.cancel(mapLists[key]);
}
for (const key in mapLists) {
colemak.forward(key);
}
},
};
const vForwardFactory = {
push: function (mapLists) {
// forward original keys
for (const key in mapLists) {
vForward.add(mapLists[key]);
}
},
map: function (mapLists) {
for (const key in mapLists) {
vColemak.map(key, mapLists[key]);
}
},
pull: function (mapLists) {
for (const key in mapLists) {
vForward.cancel(mapLists[key]);
}
for (const key in mapLists) {
vColemak.forward(key);
}
},
};
// TODO: Add more search completion source (with json)
const parseSearchResponse = function (response) {
const res = JSON.parse(response.text);
return res.map((r) => r.phrase);
};
const _addSearchAlias = function (
alias,
name,
searchUrl,
acUrl = "https://duckduckgo.com/ac/?q=",
searchPrefix = "s",
parseResponse = parseSearchResponse,
) {
api.addSearchAlias(
alias,
name,
searchUrl,
searchPrefix,
acUrl,
parseResponse,
);
};
// Shortcut for querySelector
const q = (selector) => document.querySelector(selector);
const qs = (selector) => document.querySelectorAll(selector);
// #endregion
// #region Keymap
// Normal Mode Keymap
const mapLists = {
/// scroll page
// Arrow
n: "j",
e: "k",
i: "l",
// l <-> i
l: "gi", // Focus on first input box by default
L: "I",
// k <-> n
k: "n",
K: "N",
// j <-> e
j: "e",
// PrevTab < H - I > NextTab
H: "E",
I: "R",
// E,N -> Up/Down HalfPage
N: "d",
E: "e",
// F -> Open Link in New Tab
F: "af",
// oH -> Tab History
oH: "H",
// gh/gi -> Prev/Next History
gh: "S",
gi: "D",
gl: "i", // Use `gl` to search and focus on input box
// t -> Open Link in New Tab
t: "gf",
// 缩放
zu: "zi",
zo: "ze",
zz: "zr",
};
// Visual Mode Keymap
const vMapLists = {
n: "j",
N: "J",
e: "k",
E: "K",
i: "l",
I: "L",
j: "e",
J: "E",
k: "n",
K: "N",
};
forwardFactory.push(mapLists);
forwardFactory.map(mapLists);
vForwardFactory.push(vMapLists);
vForwardFactory.map(vMapLists);
// All other unmapped keys should be defined here
// TODO: Add more mouse click keymap
api.unmap("gi"); // conflict with `gi` in `mapLists`
api.unmap("C"); // Use `F` instead (Open Link in New Tab)
api.map("g/", "gU"); // Goto Root Domain
// TODO: Add SPC keymap as leader (maybe change `,` to `SPC`)
api.unmap("<space>"); // Leader Key
forwardFactory.pull(mapLists);
vForwardFactory.pull(vMapLists);
api.map("gH", "g/");
// #endregion
// #region Omnibar NOTE: Dosn't work
// api.cmap("<Ctrl-a>", "<Ctrl-ArrowUp>");
// api.cmap("<Ctrl-e>", "<Ctrl-ArrowDown>");
// api.cmap("<Ctrl-f>", "<ArrowRight>");
// api.cmap("<Ctrl-b>", "<ArrowLeft>");
// api.cmap("<Alt-f>", "<Ctrl-ArrowRight>");
// api.cmap("<Alt-b>", "<Ctrl-ArrowLeft>");
// api.cmap("<Ctrl-h>", "<Backspace>");
// api.cmap("<Ctrl-d>", "<Delete>");
// #endregion
// #region Search Alias
const removedSearchAlias = [
"b", // Baidu
"d", // DuckDuckGo
"e", // Wikipedia
"g", // Google
"s", // StackOverflow
"w", // Bing
"y", // YouTube
];
removedSearchAlias.forEach((alias) => removeSearchAlias(alias));
const searchAliases = [
["a2", "AlternativeTo", "https://alternativeto.net/browse/search/?q="],
["ap", "APT", "https://packages.ubuntu.com/search?keywords="],
["au", "AUR", "https://aur.archlinux.org/packages?K="],
["aw", "ArchWiki", "https://wiki.archlinux.org/index.php?search="],
["bd", "Baidu", "https://www.baidu.com/s?wd="],
["bi", "Bing", "https://www.bing.com/search?q="],
["bl", "Bilibili", "https://search.bilibili.com/all?keyword="],
["br", "HomeBrew", "https://duckduckgo.com/?q=!brew "],
["cg", "ChatGPT", "https://chat.openai.com/?q="],
["cr", "Chrome Web Store", "https://chrome.google.com/webstore/search/"],
["dd", "DuckDuckGo", "https://duckduckgo.com/?q="],
["de", "Thesaurus", "https://www.onelook.com/?w="],
["eb", "ebay", "https://www.ebay.co.uk/sch/i.html?kw="],
["fe", "Felo", "https://felo.ai/search?q="],
["gh", "GitHub", "https://github.com/search?type=repositories&q="],
["gg", "Google", "https://www.google.com/search?q="],
["mc", "Metacritic", "https://www.metacritic.com/search/"],
["ng", "NuGet", "https://www.nuget.org/packages?q="],
["np", "npm", "https://www.npmjs.com/search?q="],
["pa", "Pacman", "https://archlinux.org/packages/?q="],
["pp", "Perplexity", "https://www.perplexity.ai/?q="],
["py", "pypi", "https://pypi.org/search/?q="],
["re", "Reddit", "https://www.reddit.com/search?q="],
["sc", "Scoop", "https://scoop.sh/#/apps?q="],
["se", "StackExchange", "https://stackexchange.com/search?q="],
["so", "StackOverflow", "https://stackoverflow.com/search?q="],
["st", "Steam", "https://store.steampowered.com/search/?term="],
["tw", "X", "https://twitter.com/search?q="],
["ud", "UrbanDictionary", "https://www.urbandictionary.com/define.php?term="],
["wa", "WolframAlpha", "https://www.wolframalpha.com/input/?i="],
["wg", "winget", "https://winget.ragerworks.com/search/all/"],
["wk", "Wikipedia", "https://en.wikipedia.org/w/index.php?title=Special:Search&search="],
["ww", "WantWords", "https://www.shenyandayi.com/wantWordsResult?lang=zh&query="],
["yt", "YouTube", "https://www.youtube.com/results?search_query="],
];
// Add all search aliases
searchAliases.forEach(([alias, name, url]) => {
_addSearchAlias(alias, name, url);
});
// #endregion
// #region Site-specific
// This is a global keymap
mapkey("yY", "yank link without parameter", function () {
const url = new URL(window.location.href);
Clipboard.write(url.origin + url.pathname);
});
// #region bilibili.com
mapkey(",n", "[n]ext Video", function () {
window.location.href = q("div.next-play").querySelector("a").href
}, { domain: /bilibili.com/ });
// #endregion
// #region chatgpt.com
const chatgptNewChat = function () {
var btn = q(
"div.no-draggable:nth-child(3) > span:nth-child(1) > button:nth-child(1)",
);
btn.click();
};
const chatgptStartStop = function () {
var btn = q("button.h-8:nth-child(2)");
btn.click();
};
mapkey(",n", "New Chat", chatgptNewChat, { domain: /chatgpt.com/ });
mapkey(",s", "Start/Stop Generating", chatgptStartStop, {
domain: /chatgpt.com/,
});
// #endregion
// #region chat.deepseek.com
mapkey(",s", "Toggle Sidebar", function () {
var btn = qs("div.ds-icon-button");
btn[0].click();
}, { domain: /chat.deepseek.com/ });
mapkey(",e", "[e]dit last input", function () {
var btn = qs("div.ds-icon-button");
btn[btn.length - 5].click();
}, { domain: /chat.deepseek.com/ });
mapkey(",y", "[y]ank last oupput", function () {
var btn = qs("div.ds-icon-button");
btn[btn.length - 4].click();
}, { domain: /chat.deepseek.com/ });
mapkey(",r", "[r]egenerate last output", function () {
var btn = qs("div.ds-icon-button");
btn[btn.length - 3].click();
}, { domain: /chat.deepseek.com/ });
mapkey(",n", "[n]ew Chat", function () {
window.location.href = "https://chat.deepseek.com/";
}, { domain: /chat.deepseek.com/ });
mapkey(",t", "Toggle co[t](R1)", function () {
var btns = qs("div.ds-button");
btns[0].click();
}, { domain: /chat.deepseek.com/ });
mapkey(",w", "Toggle [w]eb Search", function () {
var btns = qs("div.ds-button");
btns[1].click();
}, { domain: /chat.deepseek.com/ });
// #endregion
// #region app.follow.is
mapkey(",t", "Toggle ", function () {
var btn = qs("button.no-drag-region");
btn[btn.length - 4].click();
}, { domain: /app.follow.is/ });
mapkey(",a", "Toggle AI Summary", function () {
var btn = qs("button.no-drag-region");
btn[btn.length - 3].click();
}, { domain: /app.follow.is/ });
mapkey(",o", "Toggle Original Website", function () {
var btn = qs("button.no-drag-region");
btn[btn.length - 4].click();
}, { domain: /app.follow.is/ });
// #endregion
// #region GitHub
// utils
const gh = {};
gh.repoLink = (owner, repo) => `https://github.com/${owner}/${repo}`;
gh.pageLink = (owner, repo) => `https://${owner}.github.io/${repo}/`;
gh.sourceLink = (owner, repo, path) =>
`${gh.repoLink(owner, repo)}/tree/${path}`;
gh.rawToSource = (url) => {
const ps = url.split("/").slice(3);
return gh.sourceLink(ps[0], ps[1], ps.slice(4).join("/"));
};
// github.com
mapkey(",e", "Use Web Editor", function () {
const url = new URL(window.location.href);
url.hostname = "github.dev";
window.location.href = url.href;
}, { domain: /github.com/ });
mapkey(",E", "Use Web Editor (New Page)", function () {
const url = new URL(window.location.href);
url.hostname = "github.dev";
tabOpenLink(url.href);
}, { domain: /github.com/ });
mapkey(",p", "Switch to GitHub Page", function () {
href = window.location.href;
owner = href.split("/")[3];
repo = href.split("/")[4];
window.location.href = gh.pageLink(owner, repo);
}, { domain: /github.com/ });
/// This might be useful for Vim plugins
mapkey(",y", "[y]ank short refeference owner/repo", function () {
const href = window.location.href;
owner = href.split("/")[3];
repo = href.split("/")[4];
Clipboard.write(`${owner}/${repo}`);
}, { domain: /github.com/ });
// github.dev
mapkey(",r", "Switch to GitHub Repo", function () {
const url = new URL(window.location.href);
url.hostname = "github.com";
window.location.href = url.href;
}, { domain: /github.dev/ });
// github.io
mapkey(",r", "Switch to GitHub Repo", function () {
const href = window.location.href;
owner = href.split("/")[2].split(".")[0];
repo = href.split("/")[3];
tabOpenLink(gh.repoLink(owner, repo));
}, { domain: /github.io/ });
mapkey(",R", "Go to GitHub Repo (New tab)", function () {
const href = window.location.href;
owner = href.split("/")[2].split(".")[0];
repo = href.split("/")[3];
tabOpenLink(gh.repoLink(owner, repo));
}, { domain: /github.io/ });
// raw.githubusercontent.com
mapkey(",r", "Switch to GitHub Repo", function () {
const url = new URL(window.location.href);
var owner, repo;
owner, repo = url.pathname.split("/").slice(1, 3);
window.location.href = gh.repoLink(owner, repo);
}, { domain: /raw.githubusercontent.com/ });
mapkey(",R", "Switch to GitHub Repo", function () {
const url = new URL(window.location.href);
var owner, repo;
owner, repo = url.pathname.split("/").slice(1, 3);
tabOpenLink(gh.repoLink(owner, repo));
}, { domain: /raw.githubusercontent.com/ });
mapkey(",s", "Open Source in GitHub", function () {
window.location.href = gh.rawToSource(window.location.href);
}, { domain: /raw.githubusercontent.com/ });
mapkey(",S", "Open Source in GitHub (New Page)", function () {
tabOpenLink(gh.rawToSource(window.location.href));
}, { domain: /raw.githubusercontent.com/ });
// #endregion GitHub
//#region app.microsoft.com
// https://apps.microsoft.com/detail/9nl6kd1h33v3?hl=en-GB&gl=GB
// This is useful in `winget` (Windows Package Manager)
mapkey(",y", "[y]ank app id", function () {
const url = new URL(window.location.href);
const id = url.pathname.split("/")[2];
Clipboard.write(id);
}, { domain: /apps.microsoft.com/ });
//#endregion
// #region perplexity.ai
/**
* 0 - 网络
* 1 - 学术
* 2 - 社交
*/
unmap("<Ctrl-i>", /perplexity.ai/); // allows to use perplexity web keybindings
mapkey(",b", "Add Perplexity [b]ookmark", function () {
// button.border:nth-child(2)
q("div.sticky.left-0").querySelectorAll("button")[2].click()
}, { domain: /perplexity.ai/ });
mapkey(",w", "Toggle [w]riting/[w]eb Search", function () {
q("div.rounded-md").querySelectorAll("span")[1].click()
setTimeout(() => { // Wait for the DOM to update
qs("div.shadow-subtle div.group\\/item")[0].click();
}, 100);
}, { domain: /perplexity.ai/ });
mapkey(",s", "[s]tart Generating", function () {
var btns = qs("span.grow button");
btns[btns.length - 1].click();
}, { domain: /perplexity.ai/ });
mapkey(",y", "[y]ank Last Output", function () {
var toolbars = qs("div.mt-sm");
var last = toolbars[toolbars.length - 1];
var btns = last.querySelectorAll("button");
btns[5].click();
}, { domain: /perplexity.ai/ });
mapkey(",r", "Change model to [r]egenerate last output", function () {
var toolbars = qs("div.mt-sm");
var last = toolbars[toolbars.length - 1];
var btns = last.querySelectorAll("button");
btns[1].click();
}, { domain: /perplexity.ai/ });
mapkey(",R", "Toggle [R]easoning with [R]1", function () {
var btns = qs("span.grow button");
btns[0].click();
setTimeout(() => { // Wait for the DOM to update
qs("div.shadow-subtle div.group\\/item")[2].click();
qs("div.group\\/item")[2].click() // Mobile view
}, 100);
}, { domain: /perplexity.ai/ });
// #endregion
// #region sspai.com
unmap("[[", /sspai.com/);
unmap("]]", /sspai.com/);
unmap(",", /sspai.com/);
mapkey("[[", "Previous Page", function () {
q("button.btn-prev").click();
}, { domain: /sspai.com/ });
mapkey("]]", "Next Page", function () {
q("button.btn-next").click();
}, { domain: /sspai.com/ });
// #endregion
// #region pixiv.net
// Use site-specific paging method
unmap("[[", /pixiv.net/);
unmap("]]", /pixiv.net/);
unmap(",", /pixiv.net/);
const isArtwork = (url) => /pixiv.net\/artworks/.test(url.href)
mapkey("[[", "Previous Page", function () {
const url = new URL(window.location.href);
if (url.href === url.origin) {
return;
}
const page = url.searchParams.get("p");
const newPage = page ? parseInt(page) - 1 : 1;
url.searchParams.set("p", newPage);
window.location.href = url.href;
}, { domain: /pixiv.net/ });
mapkey("]]", "Next Page", function () {
const url = new URL(window.location.href);
if (url.href === url.origin) {
return;
}
const page = url.searchParams.get("p");
const newPage = page ? parseInt(page) + 1 : 2;
url.searchParams.set("p", newPage);
window.location.href = url.href;
}, { domain: /pixiv.net/ });
mapkey(",b", "Add to [b]ookmark", function () {
const url = new URL(window.location.href);
if (!isArtwork(url)) { return; }
const toolbar = q('section [class$="Toolbar"]')
toolbar.querySelectorAll("div")[2].querySelector("button").click()
}, { domain: /pixiv.net/ });
mapkey(",B", "Add to private [B]ookmark", function () {
const url = new URL(window.location.href);
if (!isArtwork(url)) { return; }
const toolbar = q('section [class$="Toolbar"]')
toolbar.querySelectorAll("div")[0].querySelector("button").click()
setTimeout(() => { // Wait for the DOM to update
q("div[role=menu]").querySelector("li").click()
}, 100);
}, { domain: /pixiv.net/ });
mapkey(",v", "Up[v]ote Artwork", function () {
const url = new URL(window.location.href);
if (!isArtwork(url)) { return; }
const toolbar = q('section [class$="Toolbar"]')
toolbar.querySelectorAll("div")[3].querySelector("button").click()
}, { domain: /pixiv.net/ });
mapkey(",f", "Toggle [f]ollow the author", function () {
const url = new URL(window.location.href)
if (!isArtwork(url)) { return; }
q("aside").querySelector("section").querySelector("button").click()
}, { domain: /pixiv.net/ });
// #endregion
// #region youtube.com
mapkey(",n", "[n]ext Video", function () {
window.location.href = q("ytd-compact-video-renderer").querySelector("a").href
}, { domain: /youtube.com/ });
mapkey(",v", "Up[v]ote Video", function () {
qs("like-button-view-model")[0].querySelector("button").click()
}, { domain: /youtube.com/ });
mapkey(",V", "Down[v]ote Video", function () {
qs("dislike-button-view-model")[0].querySelector("button").click()
}, { domain: /youtube.com/ });
// class="ytp-subtitles-button ytp-button"
mapkey(",c", "toggle [c]aptions", function () {
q("button.ytp-subtitles-button").click()
}, { domain: /youtube.com/ });
// #endregion
//#region zhihu.com
mapkey(",d", "Toggle [d]ark mode", function () {
const url = new URL(window.location.href);
if (url.searchParams.get("theme") === "dark") {
url.searchParams.set("theme", "light");
} else {
url.searchParams.set("theme", "dark");
}
window.location.href = url.href;
}, { domain: /zhihu.com/ });
//#endregion
// #endregion
// #region ACE Editor
addVimMapKey(
// Navigation
{
keys: "k",
type: "motion",
motion: "findNext",
motionArgs: { forward: true, toJumplist: true },
},
{
keys: "K",
type: "motion",
motion: "findNext",
motionArgs: { forward: false, toJumplist: true },
},
// Word movement
{
keys: "j",
type: "motion",
motion: "moveByWords",
motionArgs: { forward: true, wordEnd: true, inclusive: true },
},
{
keys: "J",
type: "motion",
motion: "moveByWords",
motionArgs: {
forward: true,
wordEnd: true,
bigWord: true,
inclusive: true,
},
},
// Insert mode entries
{
keys: "l",
type: "action",
action: "enterInsertMode",
isEdit: true,
actionArgs: { insertAt: "inplace" },
context: "normal",
},
{
keys: "gl",
type: "action",
action: "enterInsertMode",
isEdit: true,
actionArgs: { insertAt: "lastEdit" },
context: "normal",
},
{
keys: "L",
type: "action",
action: "enterInsertMode",
isEdit: true,
actionArgs: { insertAt: "firstNonBlank" },
context: "normal",
},
{
keys: "gL",
type: "action",
action: "enterInsertMode",
isEdit: true,
actionArgs: { insertAt: "bol" },
context: "normal",
},
{
keys: "L",
type: "action",
action: "enterInsertMode",
isEdit: true,
actionArgs: { insertAt: "startOfSelectedArea" },
context: "visual",
},
{
keys: "n",
type: "motion",
motion: "moveByLines",
motionArgs: { forward: true, linewise: true },
},
{
keys: "e",
type: "motion",
motion: "moveByLines",
motionArgs: { forward: false, linewise: true },
},
{
keys: "i",
type: "motion",
motion: "moveByCharacters",
motionArgs: { forward: true },
},
{
keys: "H",
type: "keyToKey",
toKeys: "^",
},
{
keys: "I",
type: "keyToKey",
toKeys: "$",
},
{
keys: "Y",
type: "keyToKey",
toKeys: "y$",
},
);
// #endregion
// #region Hints
api.Hints.setCharacters("qwfpgarstdcv"); // Left-hand keys
// #endregion