Este userscript mostra os atalhos do Discourse diretamente no fórum:
Ele está desativado por padrão; clique no ícone no canto superior direito para alternar. ![]()
Não foi feito para ser bonito. Apenas ative-o quando sentir vontade de usar e memorizar os atalhos ![]()
Ele não mostra todos os atalhos existentes, mas a maioria deles.
Os atalhos relacionados a favoritos não estão (ainda?) incluídos, pois há muitos deles para uma seção nicho do site.
Há alguns atalhos que não entendo na lista de atalhos ?:
-
Ctrl + l → Filtrar barra lateral
-
Seleção de tópicos:
Shift + d → Descartar tópicos selecionados -
a Inserir seleção no compositor aberto
Se alguém puder me explicar o que eles fazem ![]()
Código completo
// ==UserScript==
// @name Discourse Shortcut Overlay
// @namespace https://meta.discourse.org/
// @version 1.1
// @description Injecta dicas de kbd diretamente nos elementos da interface do Discourse para mostrar atalhos de teclado no contexto.
// @match https://meta.discourse.org/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function discourseShortcutOverlay() {
"use strict";
if (window.__dso__) {
window.__dso__.stop();
}
// ── CSS ─────────────────────────────────────────────────────────────────────
const css = document.createElement("style");
css.id = "__dso_style";
css.textContent = `
.dso-wrap {
display: inline-flex; align-items: center; gap: 2px;
pointer-events: none; flex-shrink: 0; user-select: none; vertical-align: text-bottom;
}
.dso-inline { margin-left: auto; padding-left: 6px; }
.dso-abs { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 99; }
.dso-abs-br { position: absolute; right: 4px; bottom: 4px; z-index: 99; }
.dso-abs-br.dso-hint-eq { right: unset; left: 4px; }
.dso-abs-tr { position: absolute; right: 4px; top: 4px; z-index: 99; }
.dso-wrap kbd {
display: inline-flex; align-items: center; justify-content: center;
padding: 0 5px; font: 700 11px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
color: #fff; border-radius: 4px; border-color: rgba(255,255,255,.22);
box-shadow: 0 1px 3px rgba(0,0,0,.2); min-height: 20px; white-space: nowrap; letter-spacing: 0;
}
.dso-wrap.nav kbd { background: color-mix(in srgb, var(--success) 70%, var(--secondary)); }
.dso-wrap.action kbd { background: color-mix(in srgb, var(--tertiary) 70%, var(--secondary)); }
.dso-wrap.write kbd { background: color-mix(in srgb, #8848b8 70%, var(--secondary)); }
.dso-wrap.post kbd { background: color-mix(in srgb, var(--danger) 70%, var(--secondary)); }
.dso-wrap .dso-plus { font: 700 9px/1 ui-monospace, monospace; color: rgba(255,255,255,.5); margin: 0 1px; }
.dso-wrap .dso-seq { font: 700 9px/1 ui-monospace, monospace; color: rgba(255,255,255,.45); margin: 0 2px; }
.dso-gnav-label { font: 10px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; color: rgba(255,255,255,.35); }
.dso-gnav-panel { display: flex; flex-wrap: wrap; gap: 4px 14px; padding: 4px 0 6px; vertical-align: unset; }
.dso-gnav-panel .dso-gnav-row { display: inline-flex; align-items: center; gap: 5px; }
.dso-gnav-panel .dso-gnav-keys { display: inline-flex; align-items: center; }
.timeline-container .topic-timeline .timeline-scroller-content { overflow: visible !important; }
.timeline-container .topic-timeline .timeline-date-wrapper { max-width: none !important; }
body.dso-hidden .dso-wrap { display: none !important; }
#dso-toggle-btn {
position: fixed; top: calc(14px + 3.5em); right: 14px; z-index: 99999; cursor: pointer;
background: var(--secondary); border: 1px solid var(--primary-low); border-radius: 6px;
color: var(--primary-medium); padding: 5px 7px; line-height: 1;
box-shadow: 0 2px 6px rgba(0,0,0,.1);
}
#dso-toggle-btn:hover { color: var(--primary); border-color: var(--primary-medium); }
#dso-toggle-btn.--off { color: var(--primary-low); border-color: var(--primary-very-low, var(--primary-low)); }
`;
document.head.appendChild(css);
// ── Registro de dicas ─────────────────────────────────────────────────────────────
const HINTS = [];
const $ = (s) => document.querySelector(s);
function keysToClass(keys) {
return (
"dso-hint-" +
keys
.map((k) =>
k
.replace(/⇧\s*/g, "shift-")
.replace(/↵/g, "enter")
.replace(/↓/g, "down")
.replace(/↑/g, "up")
.replace(/\//g, "slash")
.replace(/\?/g, "question")
.replace(/!/g, "bang")
.replace(/#/g, "hash")
.replace(/\./g, "dot")
.replace(/=/g, "eq")
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
)
.join("-")
);
}
// Dica de alvo único
function hint(
keys,
theme,
targetFn,
mode = "inline",
beforeSel = null,
seq = false,
title = ""
) {
HINTS.push({
keys,
theme,
targetFn,
mode,
beforeSel,
seq,
title,
hintClass: keysToClass(keys),
customBuild: null,
state: { el: null, wrap: null },
});
}
// Dica de múltiplos alvos – injeta após cada elemento correspondente a multiSel
function hintMulti(
keys,
theme,
multiSel,
mode = "after",
seq = false,
title = ""
) {
HINTS.push({
keys,
theme,
multiSel,
mode,
seq,
title,
hintClass: keysToClass(keys),
state: { pairs: [] },
});
}
// Dica de painel personalizado
function hintPanel(theme, mode, hintClass, targetFn, buildFn, opts = {}) {
HINTS.push({
keys: null,
theme,
mode,
beforeSel: opts.beforeSel ?? null,
seq: opts.seq ?? false,
hintClass,
targetFn,
customBuild: buildFn,
state: { el: null, wrap: null },
});
}
// ── Construtores DOM ──────────────────────────────────────────────────────────────
// Micro-ajuda: cria um elemento com propriedades atribuídas
function el(tag, props = {}) {
return Object.assign(document.createElement(tag), props);
}
function buildWrap(keys, theme, mode, seq = false, title = "") {
const modeClass =
{ abs: "dso-abs", "abs-br": "dso-abs-br", "abs-tr": "dso-abs-tr" }[
mode
] ?? (mode === "sibling" || mode === "after" ? "" : "dso-inline");
const wrap = el("span", {
className: `dso-wrap ${theme} ${modeClass}`.trimEnd(),
});
if (title) {
wrap.title = title;
}
keys.forEach((k, i) => {
if (i > 0) {
wrap.appendChild(
el("span", {
className: seq ? "dso-seq" : "dso-plus",
textContent: seq ? "→" : "+",
})
);
}
wrap.appendChild(el("kbd", { textContent: k }));
});
return wrap;
}
// Painel genérico: linhas de sequências de kbd com rótulos opcionais.
// usePlus: usa separadores "+" (combos) em vez de "→" (sequências).
function buildPanel(theme, entries, wrapStyle = {}, usePlus = false) {
const wrap = el("span", { className: `dso-wrap ${theme}` });
Object.assign(wrap.style, wrapStyle);
for (const { keys, label } of entries) {
const row = el("span");
Object.assign(row.style, {
display: "inline-flex",
alignItems: "center",
gap: "4px",
});
keys.forEach((k, i) => {
if (i > 0) {
row.appendChild(
el("span", {
className: usePlus ? "dso-plus" : "dso-seq",
textContent: usePlus ? "+" : "→",
})
);
}
row.appendChild(el("kbd", { textContent: k }));
});
if (label) {
row.appendChild(
el("span", { className: "dso-gnav-label", textContent: label })
);
}
wrap.appendChild(row);
}
return wrap;
}
// G-nav usa classes CSS dedicadas para seu layout de envolvimento
function buildGNavPanel() {
const panel = el("div", { className: "dso-wrap dso-gnav-panel nav" });
for (const { keys, label } of [
{ keys: ["u"], label: "Voltar" },
{ keys: ["g", "h"], label: "Início" },
{ keys: ["g", "l"], label: "Mais recentes" },
{ keys: ["g", "n"], label: "Novos" },
{ keys: ["g", "u"], label: "Não lidos" },
{ keys: ["g", "y"], label: "Não vistos" },
{ keys: ["g", "c"], label: "Categorias" },
{ keys: ["g", "t"], label: "Top" },
]) {
const kg = el("span", { className: "dso-gnav-keys" });
keys.forEach((k, i) => {
if (i > 0) {
kg.appendChild(
el("span", { className: "dso-seq", textContent: "→" })
);
}
kg.appendChild(el("kbd", { textContent: k }));
});
const row = el("div", { className: "dso-gnav-row" });
row.appendChild(kg);
row.appendChild(
el("span", { className: "dso-gnav-label", textContent: label })
);
panel.appendChild(row);
}
return panel;
}
// ── Lógica de injeção ───────────────────────────────────────────────────────────
function inject(h) {
const target = h.targetFn();
const { state, keys, theme, mode, beforeSel } = h;
if (target === state.el) {
return;
} // sem mudança – já injetado
if (state.wrap) {
state.wrap.remove();
}
if (state.el?.dataset.dsoPos !== undefined) {
state.el.style.position = state.el.dataset.dsoPos || "";
delete state.el.dataset.dsoPos;
}
state.el = target;
state.wrap = null;
if (!target) {
return;
}
if (mode === "abs" || mode === "abs-br" || mode === "abs-tr") {
if (getComputedStyle(target).position === "static") {
target.dataset.dsoPos = "";
target.style.position = "relative";
}
}
const wrap = h.customBuild
? h.customBuild()
: buildWrap(keys, theme, mode, h.seq, h.title);
if (h.hintClass) {
wrap.classList.add(h.hintClass);
}
let insertParent, insertRef;
if (mode === "after") {
insertParent = target.parentNode;
insertRef = target.nextSibling;
} else {
const beforeEl = beforeSel ? target.querySelector(beforeSel) : null;
insertParent = beforeEl ? beforeEl.parentNode : target;
insertRef = beforeEl;
}
insertParent.insertBefore(wrap, insertRef);
state.wrap = wrap;
}
function injectMulti(h) {
const { state } = h;
const targets = new Set(document.querySelectorAll(h.multiSel));
state.pairs = state.pairs.filter(({ el: e, wrap }) => {
if (!targets.has(e)) {
wrap.remove();
return false;
}
return true;
});
const covered = new Set(state.pairs.map((p) => p.el));
targets.forEach((target) => {
if (covered.has(target)) {
return;
}
const wrap = buildWrap(h.keys, h.theme, h.mode, h.seq, h.title);
if (h.hintClass) {
wrap.classList.add(h.hintClass);
}
target.parentNode?.insertBefore(wrap, target.nextSibling);
state.pairs.push({ el: target, wrap });
});
}
// ────────────────────────────────────────────────────────────────────────────
// DEFINIÇÕES DE DICAS
// ────────────────────────────────────────────────────────────────────────────
// ── Cabeçalho ────────────────────────────────────────────────────────────────
// / → pesquisa (oculto quando a entrada está em foco)
hint(
["/"],
"action",
() => {
const input = $("#header-search-input");
if (!input || input === document.activeElement) {
return null;
}
return input.closest(".search-input--header")?.parentElement ?? null;
},
"sibling",
".show-advanced-search",
false,
"Pesquisar"
);
// ↑ / ↓ → navegar resultados da pesquisa
hint(
["↑ / ↓"],
"action",
() =>
$(".search-input--header")
? $(".search-menu-initial-options, .search-menu-assistant") || null
: null,
"abs-tr",
null,
false,
"Navegar resultados"
);
// Ctrl+↵ → abrir pesquisa em página inteira (apenas quando a entrada está em foco)
hint(
["Ctrl", "↵"],
"action",
() => {
const input = $("#header-search-input");
if (!input || input !== document.activeElement) {
return null;
}
return input;
},
"after",
null,
false,
"Pesquisa em página inteira"
);
// c → novo tópico
hint(
["c"],
"write",
() => $("#create-topic"),
"inline",
null,
false,
"Novo tópico"
);
// ? → modal de atalhos de teclado
hint(
["?"],
"action",
() => $(".keyboard-shortcuts-btn, button[aria-label='Keyboard shortcuts']"),
"inline",
null,
false,
"Atalhos"
);
// . → carregar banner de tópicos novos/atualizados
hint(
["."],
"action",
() => $(".show-more.has-topics a"),
"inline",
null,
false,
"Carregar novos tópicos"
);
// ── Lista de tópicos ────────────────────────────────────────────────────────
// Exclui .more-topics__list / .suggested-topics para que as dicas não vazem para essa área.
function mainListRows() {
return [...document.querySelectorAll(".topic-list-item")].filter(
(r) => !r.closest(".more-topics__list, .suggested-topics, .more-topics")
);
}
hint(
["j ↓"],
"nav",
() => {
const rows = mainListRows();
if (!rows.length) {
return null;
}
const selIdx = rows.findIndex((r) => r.classList.contains("selected"));
if (selIdx === -1) {
return rows[0]?.querySelector(".main-link") || null;
}
if (selIdx >= rows.length - 1) {
return null;
}
return rows[selIdx + 1]?.querySelector(".main-link") || null;
},
"abs",
null,
false,
"Próximo tópico"
);
hint(
["k ↑"],
"nav",
() => {
const rows = mainListRows();
if (!rows.length) {
return null;
}
const selIdx = rows.findIndex((r) => r.classList.contains("selected"));
if (selIdx <= 0) {
return null;
}
return rows[selIdx - 1]?.querySelector(".main-link") || null;
},
"abs",
null,
false,
"Tópico anterior"
);
// o / Enter → abrir tópico selecionado
hint(
["o / ↵"],
"nav",
() =>
$(
".topic-list tr.selected a.title, .topic-list-item.selected .main-link a, .topic-list-item.selected .topic-title a"
),
"inline",
null,
false,
"Abrir tópico"
);
// ⇧j / ⇧k → próxima/anterior seção de navegação por abas
hint(
["⇧ j"],
"nav",
() => {
const pills = [...document.querySelectorAll(".nav.nav-pills li")];
const i = pills.findIndex((p) => p.classList.contains("active"));
return pills[i + 1] || null;
},
"inline",
null,
false,
"Próxima seção"
);
hint(
["⇧ k"],
"nav",
() => {
const pills = [...document.querySelectorAll(".nav.nav-pills li")];
const i = pills.findIndex((p) => p.classList.contains("active"));
return i > 0 ? pills[i - 1] : null;
},
"inline",
null,
false,
"Seção anterior"
);
// ── Compositor ────────────────────────────────────────────────────────────────
hint(
["Ctrl", "↵"],
"write",
() =>
$(".save-or-cancel .create, .reply-area .create, #reply-control .create"),
"inline",
null,
false,
"Enviar"
);
hint(
["Esc"],
"write",
() => $(".save-or-cancel .cancel, .reply-area .cancel"),
"inline",
null,
false,
"Cancelar"
);
// ⇧c → retornar ao compositor minimizado
hint(
["⇧ c"],
"write",
() => $("#reply-control.draft .draft-text"),
"inline",
null,
false,
"Retornar ao compositor"
);
// ⇧F11 → tela cheia (oculto quando o compositor é um rascunho)
hint(
["⇧ F11"],
"write",
() => ($("#reply-control.draft") ? null : $(".toggle-fullscreen")),
"after",
null,
false,
"Tela cheia"
);
hint(
["Esc"],
"write",
() => $(".toggle-minimize"),
"after",
null,
false,
"Minimizar"
);
// ── Opções do compositor (+) popup ────────────────────────────────────────────
hint(
["Ctrl", "e"],
"write",
() => $('[data-name="format-code"]'),
"abs-br",
null,
false,
"Texto pré-formatado"
);
hint(
["Ctrl", "⇧ 8"],
"write",
() => $('[data-name="apply-unordered-list"]'),
"abs-br",
null,
false,
"Lista com marcadores"
);
hint(
["Ctrl", "⇧ 7"],
"write",
() => $('[data-name="apply-ordered-list"]'),
"abs-br",
null,
false,
"Lista numerada"
);
hint(
["Ctrl", "Alt", "0"],
"write",
() => $('[data-name="heading-paragraph"]'),
"abs-br",
null,
false,
"Parágrafo"
);
hint(
["Ctrl", "Alt", "1"],
"write",
() => $('[data-name="heading-1"]'),
"abs-br",
null,
false,
"Título 1"
);
hint(
["Ctrl", "Alt", "2"],
"write",
() => $('[data-name="heading-2"]'),
"abs-br",
null,
false,
"Título 2"
);
hint(
["Ctrl", "Alt", "3"],
"write",
() => $('[data-name="heading-3"]'),
"abs-br",
null,
false,
"Título 3"
);
hint(
["Ctrl", "Alt", "4"],
"write",
() => $('[data-name="heading-4"]'),
"abs-br",
null,
false,
"Título 4"
);
hint(
["Ctrl", "l"],
"write",
() => $('.toolbar__button[title="Hyperlink"]'),
"abs-br",
null,
false,
"Inserir link"
);
// ── Ações de postagem (injetadas em cada postagem visível) ────────────────────
hintMulti(
["r"],
"post",
".topic-post .post-controls button.reply",
"after",
false,
"Responder à postagem"
);
hintMulti(
["e"],
"post",
".topic-post .post-controls button.edit",
"after",
false,
"Editar postagem"
);
hintMulti(
["l"],
"post",
".topic-post .discourse-reactions-double-button, .topic-post .post-controls button.toggle-like",
"after",
false,
"Curtir postagem"
);
hintMulti(
["b"],
"post",
".topic-post .post-controls .post-action-menu__bookmark",
"after",
false,
"Favoritar postagem"
);
hintMulti(
["s"],
"post",
".topic-post a.post-date",
"after",
false,
"Compartilhar postagem"
);
hintMulti(
["q"],
"post",
".topic-post .post-controls button.quote-post",
"after",
false,
"Citar postagem"
);
hintMulti(
["!"],
"post",
".topic-post .post-controls .post-action-menu__flag, .topic-post .post-controls button.create-flag",
"after",
false,
"Denunciar postagem"
);
hintMulti(
["d"],
"post",
".topic-post .post-controls .post-action-menu__delete, .topic-post .post-controls button.delete",
"after",
false,
"Excluir postagem"
);
// ── Visualização de tópico – navegação de postagens ──────────────────────────
hint(
["j ↓"],
"nav",
() => {
if (!$(".timeline-container")) {
return null;
}
const posts = [...document.querySelectorAll(".topic-post")];
const selIdx = posts.findIndex((p) => p.classList.contains("selected"));
if (selIdx === -1) {
return posts[0] || null;
}
return selIdx < posts.length - 1 ? posts[selIdx + 1] : null;
},
"abs",
null,
false,
"Próxima postagem"
);
hint(
["k ↑"],
"nav",
() => {
if (!$(".timeline-container")) {
return null;
}
const posts = [...document.querySelectorAll(".topic-post")];
const selIdx = posts.findIndex((p) => p.classList.contains("selected"));
return selIdx > 0 ? posts[selIdx - 1] : null;
},
"abs",
null,
false,
"Postagem anterior"
);
// j/k no roletador da linha do tempo – posicionado à sua direita
hintPanel(
"nav",
"abs",
"dso-hint-timeline-jk",
() => $(".timeline-container") && ($(".timeline-scroller-content") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["k ↑"], label: "Postagem anterior" },
{ keys: ["j ↓"], label: "Próxima postagem" },
],
{
position: "absolute",
left: "calc(100% + 8px)",
top: "50%",
transform: "translateY(-50%)",
flexDirection: "column",
gap: "3px",
zIndex: "99",
}
)
);
// # → ir para número da postagem
hint(
["#"],
"nav",
() => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
"inline",
null,
false,
"Ir para postagem"
);
// ⇧l → ir para primeiro não lido – colocado dentro da área #
hint(
["⇧ l"],
"nav",
() => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
"inline",
null,
false,
"Primeiro não lido"
);
// ⇧r → responder ao tópico (botão do rodapé)
hint(
["⇧ r"],
"post",
() =>
$(
".topic-footer-main-buttons button.btn-primary.create, #topic-footer-buttons .btn.reply"
),
"inline",
null,
false,
"Responder ao tópico"
);
// ── Navegação G ──────────────────────────────────────────────────────────────
// u + g→h … g→t – painel injetado acima de .nav-pills
hintPanel(
"nav",
"sibling",
"dso-hint-gnav",
() =>
$(".navigation-container, .list-controls .container, .navigation-bar"),
buildGNavPanel,
{ beforeSel: ".nav-pills", seq: true }
);
hint(
["g", "b"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/bookmarks'], .user-menu a[href*='/bookmarks']"
),
"inline",
null,
true,
"Favoritos"
);
hint(
["g", "m"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/messages'], .user-menu a[href*='/messages']"
),
"inline",
null,
true,
"Mensagens"
);
hint(
["g", "d"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/drafts'], .user-menu a[href*='/drafts']"
),
"inline",
null,
true,
"Rascunhos"
);
hint(
["g", "p"],
"nav",
() => $("#user-menu-button-profile"),
"inline",
null,
true,
"Perfil"
);
// g→k / g→j – painel de tópico anterior/próximo ao lado do título do tópico
hintPanel(
"nav",
"abs",
"dso-hint-topic-nav",
() =>
$(".timeline-container") &&
($(".title-wrapper #topic-title, .title-wrapper .topic-title") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["g", "k"], label: "Tópico anterior" },
{ keys: ["g", "j"], label: "Próximo tópico" },
],
{
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
flexDirection: "column",
gap: "4px",
alignItems: "flex-start",
zIndex: "99",
}
),
{ seq: true }
);
// ── Botões do cabeçalho ──────────────────────────────────────────────────────
hint(
["="],
"action",
() => $(".header-sidebar-toggle")?.parentElement ?? null,
"abs-br",
null,
false,
"Barra lateral"
);
hint(
["p"],
"action",
() => $("#current-user"),
"abs-br",
null,
false,
"Perfil"
);
// ── Ações do tópico ──────────────────────────────────────────────────────────
// f → favoritar tópico
hint(
["f"],
"post",
() =>
$(".timeline-container") &&
($('.topic-footer-main-buttons [data-identifier="bookmark-menu"]') ||
null),
"inline",
null,
false,
"Favoritar tópico"
);
hint(
["⇧ s"],
"post",
() => $("#topic-footer-buttons button.share-and-invite"),
"inline",
null,
false,
"Compartilhar tópico"
);
hint(
["⇧ p"],
"nav",
() => $(".pinned-button button"),
"inline",
null,
false,
"Fixar/Destacar"
);
hint(
["⇧ u"],
"nav",
() =>
$(".timeline-container") &&
($("#topic-footer-buttons button.defer-topic") || null),
"inline",
null,
false,
"Marcar como não lido"
);
hint(
["⇧ a"],
"action",
() => $(".toggle-admin-menu"),
"abs-br",
null,
false,
"Ações de administrador"
);
hint(
["a"],
"action",
() =>
$(".timeline-container") &&
($("#topic-footer-buttons button.archive-topic") || null),
"inline",
null,
false,
"Arquivar"
);
// m→w / m→t / m→r / m→m – painel de nível de notificação
hintPanel(
"nav",
"inline",
"dso-hint-tracking",
() => $(".timeline-container") && ($(".timeline-footer-controls") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["m", "w"], label: "Observar" },
{ keys: ["m", "t"], label: "Rastrear" },
{ keys: ["m", "r"], label: "Normal" },
{ keys: ["m", "m"], label: "Silenciar" },
],
{ flexDirection: "column", gap: "3px" }
),
{ seq: true }
);
// ── Seleção em massa ─────────────────────────────────────────────────────────
hint(
["⇧ b"],
"action",
() => $("button.bulk-select"),
"abs-br",
null,
false,
"Seleção em massa"
);
hint(
["⇧ d"],
"action",
() => $("#dismiss-topics-top, #dismiss-new-top, .dismiss-read"),
"inline",
null,
false,
"Descartar"
);
hint(
["x"],
"nav",
() =>
$(".topic-list-item.selected td.bulk-select") ||
$(".topic-list-item td.bulk-select"),
"abs-br",
null,
false,
"Selecionar linha"
);
// ── Desconectar ──────────────────────────────────────────────────────────────
hint(
["⇧ z", "⇧ z"],
"action",
() => $("li.logout button"),
"inline",
null,
true,
"Sair"
);
// ── Chat ─────────────────────────────────────────────────────────────────────
hint(
["-"],
"action",
() => $(".chat-header-icon"),
"abs-br",
null,
false,
"Alternar chat"
);
// Alt+↑/↓, Alt+⇧↑/↓, ⇧Esc, Ctrl+K – painel anexado ao gaveta do chat
hintPanel(
"action",
"inline",
"dso-hint-chat-composer",
() => $(".chat-drawer-content"),
() =>
buildPanel(
"action",
[
{ keys: ["Ctrl", "k"], label: "Canal rápido" },
{ keys: ["Alt", "↑ / ↓"], label: "Trocar canal" },
{ keys: ["Alt", "⇧ ↑ / ↓"], label: "Trocar não lidos" },
{ keys: ["⇧", "Esc"], label: "Marcar todos como lidos" },
],
{
flexWrap: "wrap",
gap: "6px 12px",
padding: "4px 6px",
borderTop: "1px solid rgba(255,255,255,.08)",
marginTop: "2px",
},
true
)
);
hint(
["Esc"],
"action",
() => $(".c-navbar__close-drawer-button"),
"after",
null,
false,
"Fechar chat"
);
// ── Botão de alternância ─────────────────────────────────────────────────────
let visible = localStorage.getItem("dso-visible") === "true";
const toggleBtn = el("button", {
id: "dso-toggle-btn",
title: "Alternar sobreposição de atalhos",
textContent: "⌨",
});
if (!visible) {
toggleBtn.classList.add("--off");
}
document.body.appendChild(toggleBtn);
toggleBtn.addEventListener("click", () => {
window.__dso__.toggle();
});
// ── MutationObserver ──────────────────────────────────────────────────────────
let rafPending = false;
function scheduleRefresh() {
if (!rafPending) {
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
HINTS.forEach((h) => (h.multiSel ? injectMulti(h) : inject(h)));
});
}
}
const observer = new MutationObserver((mutations) => {
const onlyOurs = mutations.every((m) => {
if (m.type === "attributes") {
return false;
}
return [...m.addedNodes, ...m.removedNodes].every(
(n) => n.nodeType !== 1 || n.classList?.contains("dso-wrap")
);
});
if (!onlyOurs) {
scheduleRefresh();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class"],
});
document.addEventListener("focusin", scheduleRefresh, { passive: true });
document.addEventListener("focusout", scheduleRefresh, { passive: true });
document.body.classList.toggle("dso-hidden", !visible);
HINTS.forEach((h) => (h.multiSel ? injectMulti(h) : inject(h)));
// ── API Pública ───────────────────────────────────────────────────────────────
window.__dso__ = {
stop() {
observer.disconnect();
document.removeEventListener("focusin", scheduleRefresh);
document.removeEventListener("focusout", scheduleRefresh);
HINTS.forEach(({ state }) => {
state.wrap?.remove();
state.pairs?.forEach(({ wrap }) => wrap.remove());
if (state.el?.dataset.dsoPos !== undefined) {
state.el.style.position = state.el.dataset.dsoPos || "";
delete state.el.dataset.dsoPos;
}
});
toggleBtn.remove();
document.body.classList.remove("dso-hidden");
css.remove();
delete window.__dso__;
console.log("%c[DSO] removido.", "color:#888");
},
toggle() {
visible = !visible;
localStorage.setItem("dso-visible", visible);
toggleBtn.classList.toggle("--off", !visible);
document.body.classList.toggle("dso-hidden", !visible);
},
};
console.log(
"%c[DSO] Discourse Shortcut Overlay ativo.",
"color:#6af;font-weight:bold"
);
})();
Substitua ou adicione correspondências de URLs (// @match https://meta.discourse.org/*) se quiser usá-lo em outro fórum Discourse.
Para instalar userscripts, use uma extensão de navegador: Greasemonkey, Tampermonkey, ScriptCat ou qualquer software externo que forneça essa funcionalidade, por exemplo, AdGuard.





