# Superposición de atajos: un userscript para ayudarte a aprender los atajos de teclado de Discourse

**URL:** https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588
**Category:** Extras
**Tags:** keyboard-shortcuts
**Created:** [20 Febrero, 2026 11:43 UTC](https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588 "2026-02-20T11:43:00Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![Canapin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/canapin/32/119591_2.png) [@Canapin](https://meta.discourse.org/u/Canapin)
#### Post date: [20 Febrero, 2026 11:43 UTC](https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588/1 "2026-02-20T11:43:00Z")

</div>

> **[Discourse Shortcut Overlay](https://greasyfork.org/en/scripts/566848-discourse-shortcut-overlay)**
>
> Injects kbd hints directly into Discourse UI elements to show keyboard shortcuts in context.

Este userscript muestra los atajos de Discourse directamente en el foro:

 ![image](https://global.discourse-cdn.com/meta/original/4X/b/3/6/b363c055d3e9389050682f9351e7d8488e78cca8.png)  
 ![image](https://global.discourse-cdn.com/meta/original/4X/2/0/1/201fdbb291a6e641176eb6c3443a3a7be05c0b87.png)

Está desactivado por defecto, haz clic en el icono superior derecho para activarlo/desactivarlo. ![image](https://global.discourse-cdn.com/meta/original/4X/d/1/c/d1c9822ec764e6333b044132742a7ed3f1c5b6bb.png)

No está pensado para ser bonito. Simplemente actívalo cuando sientas que quieres usar y memorizar los atajos 🙂

No muestra _todos_ los atajos existentes, pero sí la mayoría.  
Los atajos relacionados con marcadores no están (¿todavía?) incluidos, hay muchos para lo que es una sección de nicho del sitio.

Hay unos pocos atajos que no entiendo en la lista de atajos ?:

- Ctrl + l → Filtrar barra lateral

- Selección de temas:  
Shift + d → Descartar temas seleccionados

- a Insertar selección en el compositor abierto

Si alguien puede explicarme qué hacen 🤔

> **Código completo**
>
> ```js
> // ==UserScript==
> // @name Discourse Shortcut Overlay
> // @namespace https://meta.discourse.org/
> // @version 1.1
> // @description Inyecta pistas kbd directamente en los elementos de la interfaz de Discourse para mostrar atajos de teclado en 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 pistas ─────────────────────────────────────────────────────────────
> 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("-")
> );
> }
> 
> // Pista de objetivo ú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 },
> });
> }
> 
> // Pista de múltiples objetivos – inyecta después de cada elemento que coincida con multiSel
> function hintMulti(
> keys,
> theme,
> multiSel,
> mode = "after",
> seq = false,
> title = ""
> ) {
> HINTS.push({
> keys,
> theme,
> multiSel,
> mode,
> seq,
> title,
> hintClass: keysToClass(keys),
> state: { pairs: [] },
> });
> }
> 
> // Pista de panel 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 },
> });
> }
> 
> // ── Constructores DOM ──────────────────────────────────────────────────────────────
> 
> // Micro-ayuda: crear un elemento con propiedades asignadas
> 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;
> }
> 
> // Panel genérico: filas de secuencias kbd con etiquetas opcionales.
> // usePlus: usa separadores "+" (combinaciones) en lugar de "→" (secuencias).
> 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 clases CSS dedicadas para su diseño de envoltura
> function buildGNavPanel() {
> const panel = el("div", { className: "dso-wrap dso-gnav-panel nav" });
> for (const { keys, label } of [
> { keys: ["u"], label: "Volver" },
> { keys: ["g", "h"], label: "Inicio" },
> { keys: ["g", "l"], label: "Más recientes" },
> { keys: ["g", "n"], label: "Nuevos" },
> { keys: ["g", "u"], label: "Sin leer" },
> { keys: ["g", "y"], label: "No vistos" },
> { keys: ["g", "c"], label: "Categorías" },
> { 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 inyección ───────────────────────────────────────────────────────────
> function inject(h) {
> const target = h.targetFn();
> const { state, keys, theme, mode, beforeSel } = h;
> 
> if (target === state.el) {
> return;
> } // sin cambio – ya inyectado
> 
> 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 });
> });
> }
> 
> // ────────────────────────────────────────────────────────────────────────────
> // DEFINICIONES DE PISTAS
> // ────────────────────────────────────────────────────────────────────────────
> 
> // ── Encabezado ───────────────────────────────────────────────────────────────────
> 
> // / → búsqueda (oculto cuando la entrada tiene 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,
> "Buscar"
> );
> 
> // ↑ / ↓ → navegar resultados de búsqueda
> hint(
> ["↑ / ↓"],
> "action",
> () =>
> $(".search-input--header")
> ? $(".search-menu-initial-options, .search-menu-assistant") || null
> : null,
> "abs-tr",
> null,
> false,
> "Navegar resultados"
> );
> 
> // Ctrl+↵ → abrir búsqueda de página completa (solo cuando la entrada tiene foco)
> hint(
> ["Ctrl", "↵"],
> "action",
> () => {
> const input = $("#header-search-input");
> if (!input || input !== document.activeElement) {
> return null;
> }
> return input;
> },
> "after",
> null,
> false,
> "Búsqueda de página completa"
> );
> 
> // c → nuevo tema
> hint(
> ["c"],
> "write",
> () => $("#create-topic"),
> "inline",
> null,
> false,
> "Nuevo tema"
> );
> 
> // ? → modal de atajos de teclado
> hint(
> ["?"],
> "action",
> () => $(".keyboard-shortcuts-btn, button[aria-label='Atajos de teclado']"),
> "inline",
> null,
> false,
> "Atajos"
> );
> 
> // . → cargar banner de temas nuevos/actualizados
> hint(
> ["."],
> "action",
> () => $(".show-more.has-topics a"),
> "inline",
> null,
> false,
> "Cargar nuevos temas"
> );
> 
> // ── Lista de temas ───────────────────────────────────────────────────────────────
> // Excluye .more-topics__list / .suggested-topics para que las pistas no se filtren a esa á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,
> "Siguiente tema"
> );
> 
> 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,
> "Tema anterior"
> );
> 
> // o / Enter → abrir tema seleccionado
> 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 tema"
> );
> 
> // ⇧j / ⇧k → siguiente/anterior sección de pestaña de navegación
> 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,
> "Siguiente sección"
> );
> 
> 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,
> "Sección 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 → volver al compositor minimizado
> hint(
> ["⇧ c"],
> "write",
> () => $("#reply-control.draft .draft-text"),
> "inline",
> null,
> false,
> "Volver al compositor"
> );
> 
> // ⇧F11 → pantalla completa (oculto cuando el compositor es un borrador)
> hint(
> ["⇧ F11"],
> "write",
> () => ($("#reply-control.draft") ? null : $(".toggle-fullscreen")),
> "after",
> null,
> false,
> "Pantalla completa"
> );
> 
> hint(
> ["Esc"],
> "write",
> () => $(".toggle-minimize"),
> "after",
> null,
> false,
> "Minimizar"
> );
> 
> // ── Opciones del compositor (+) ventana emergente ────────────────────────────────────────────────
> 
> hint(
> ["Ctrl", "e"],
> "write",
> () => $('[data-name="format-code"]'),
> "abs-br",
> null,
> false,
> "Texto preformateado"
> );
> hint(
> ["Ctrl", "⇧ 8"],
> "write",
> () => $('[data-name="apply-unordered-list"]'),
> "abs-br",
> null,
> false,
> "Lista con viñetas"
> );
> hint(
> ["Ctrl", "⇧ 7"],
> "write",
> () => $('[data-name="apply-ordered-list"]'),
> "abs-br",
> null,
> false,
> "Lista ordenada"
> );
> hint(
> ["Ctrl", "Alt", "0"],
> "write",
> () => $('[data-name="heading-paragraph"]'),
> "abs-br",
> null,
> false,
> "Párrafo"
> );
> 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="Hipervínculo"]'),
> "abs-br",
> null,
> false,
> "Insertar enlace"
> );
> 
> // ── Acciones de publicación (inyectadas en cada publicación visible) ─────────────────────────────
> 
> hintMulti(
> ["r"],
> "post",
> ".topic-post .post-controls button.reply",
> "after",
> false,
> "Responder a la publicación"
> );
> hintMulti(
> ["e"],
> "post",
> ".topic-post .post-controls button.edit",
> "after",
> false,
> "Editar publicación"
> );
> hintMulti(
> ["l"],
> "post",
> ".topic-post .discourse-reactions-double-button, .topic-post .post-controls button.toggle-like",
> "after",
> false,
> "Me gusta la publicación"
> );
> hintMulti(
> ["b"],
> "post",
> ".topic-post .post-controls .post-action-menu__bookmark",
> "after",
> false,
> "Marcar publicación"
> );
> hintMulti(
> ["s"],
> "post",
> ".topic-post a.post-date",
> "after",
> false,
> "Compartir publicación"
> );
> hintMulti(
> ["q"],
> "post",
> ".topic-post .post-controls button.quote-post",
> "after",
> false,
> "Citar publicación"
> );
> hintMulti(
> ["!"],
> "post",
> ".topic-post .post-controls .post-action-menu__flag, .topic-post .post-controls button.create-flag",
> "after",
> false,
> "Reportar publicación"
> );
> hintMulti(
> ["d"],
> "post",
> ".topic-post .post-controls .post-action-menu__delete, .topic-post .post-controls button.delete",
> "after",
> false,
> "Eliminar publicación"
> );
> 
> // ── Vista de tema – navegación de publicaciones ──────────────────────────────────────────────
> 
> 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,
> "Siguiente publicación"
> );
> 
> 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,
> "Publicación anterior"
> );
> 
> // j/k en el desplazador de línea de tiempo – posicionado a su derecha
> hintPanel(
> "nav",
> "abs",
> "dso-hint-timeline-jk",
> () => $(".timeline-container") && ($(".timeline-scroller-content") || null),
> () =>
> buildPanel(
> "nav",
> [
> { keys: ["k ↑"], label: "Publicación anterior" },
> { keys: ["j ↓"], label: "Siguiente publicación" },
> ],
> {
> position: "absolute",
> left: "calc(100% + 8px)",
> top: "50%",
> transform: "translateY(-50%)",
> flexDirection: "column",
> gap: "3px",
> zIndex: "99",
> }
> )
> );
> 
> // # → saltar al número de publicación
> hint(
> ["#"],
> "nav",
> () => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
> "inline",
> null,
> false,
> "Saltar a publicación"
> );
> 
> // ⇧l → ir a la primera sin leer – colocada dentro del área #
> hint(
> ["⇧ l"],
> "nav",
> () => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
> "inline",
> null,
> false,
> "Primera sin leer"
> );
> 
> // ⇧r → responder al tema (botón del pie de página)
> hint(
> ["⇧ r"],
> "post",
> () =>
> $(
> ".topic-footer-main-buttons button.btn-primary.create, #topic-footer-buttons .btn.reply"
> ),
> "inline",
> null,
> false,
> "Responder al tema"
> );
> 
> // ── Navegación G ──────────────────────────────────────────────────────────────
> 
> // u + g→h … g→t – panel inyectado encima 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,
> "Marcadores"
> );
> 
> hint(
> ["g", "m"],
> "nav",
> () =>
> $(
> ".sidebar-section-link-wrapper a[href*='/messages'], .user-menu a[href*='/messages']"
> ),
> "inline",
> null,
> true,
> "Mensajes"
> );
> 
> hint(
> ["g", "d"],
> "nav",
> () =>
> $(
> ".sidebar-section-link-wrapper a[href*='/drafts'], .user-menu a[href*='/drafts']"
> ),
> "inline",
> null,
> true,
> "Borradores"
> );
> 
> hint(
> ["g", "p"],
> "nav",
> () => $("#user-menu-button-profile"),
> "inline",
> null,
> true,
> "Perfil"
> );
> 
> // g→k / g→j – panel de tema anterior/siguiente junto al título del tema
> hintPanel(
> "nav",
> "abs",
> "dso-hint-topic-nav",
> () =>
> $(".timeline-container") &&
> ($(".title-wrapper #topic-title, .title-wrapper .topic-title") || null),
> () =>
> buildPanel(
> "nav",
> [
> { keys: ["g", "k"], label: "Tema anterior" },
> { keys: ["g", "j"], label: "Siguiente tema" },
> ],
> {
> position: "absolute",
> right: "8px",
> top: "50%",
> transform: "translateY(-50%)",
> flexDirection: "column",
> gap: "4px",
> alignItems: "flex-start",
> zIndex: "99",
> }
> ),
> { seq: true }
> );
> 
> // ── Botones del encabezado ────────────────────────────────────────────────────────────
> 
> hint(
> ["="],
> "action",
> () => $(".header-sidebar-toggle")?.parentElement ?? null,
> "abs-br",
> null,
> false,
> "Barra lateral"
> );
> hint(
> ["p"],
> "action",
> () => $("#current-user"),
> "abs-br",
> null,
> false,
> "Perfil"
> );
> 
> // ── Acciones del tema ─────────────────────────────────────────────────────────────
> 
> // f → marcar tema
> hint(
> ["f"],
> "post",
> () =>
> $(".timeline-container") &&
> ($('.topic-footer-main-buttons [data-identifier="bookmark-menu"]') ||
> null),
> "inline",
> null,
> false,
> "Marcar tema"
> );
> 
> hint(
> ["⇧ s"],
> "post",
> () => $("#topic-footer-buttons button.share-and-invite"),
> "inline",
> null,
> false,
> "Compartir tema"
> );
> hint(
> ["⇧ p"],
> "nav",
> () => $(".pinned-button button"),
> "inline",
> null,
> false,
> "Fijar/Desfijar"
> );
> hint(
> ["⇧ u"],
> "nav",
> () =>
> $(".timeline-container") &&
> ($("#topic-footer-buttons button.defer-topic") || null),
> "inline",
> null,
> false,
> "Marcar como no leído"
> );
> hint(
> ["⇧ a"],
> "action",
> () => $(".toggle-admin-menu"),
> "abs-br",
> null,
> false,
> "Acciones de administrador"
> );
> hint(
> ["a"],
> "action",
> () =>
> $(".timeline-container") &&
> ($("#topic-footer-buttons button.archive-topic") || null),
> "inline",
> null,
> false,
> "Archivar"
> );
> 
> // m→w / m→t / m→r / m→m – panel de nivel de notificación
> hintPanel(
> "nav",
> "inline",
> "dso-hint-tracking",
> () => $(".timeline-container") && ($(".timeline-footer-controls") || null),
> () =>
> buildPanel(
> "nav",
> [
> { keys: ["m", "w"], label: "Observar" },
> { keys: ["m", "t"], label: "Seguir" },
> { keys: ["m", "r"], label: "Normal" },
> { keys: ["m", "m"], label: "Silenciar" },
> ],
> { flexDirection: "column", gap: "3px" }
> ),
> { seq: true }
> );
> 
> // ── Selección masiva ───────────────────────────────────────────────────────────────
> 
> hint(
> ["⇧ b"],
> "action",
> () => $("button.bulk-select"),
> "abs-br",
> null,
> false,
> "Selección masiva"
> );
> 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,
> "Seleccionar fila"
> );
> 
> // ── Cerrar sesión ────────────────────────────────────────────────────────────────────
> 
> hint(
> ["⇧ z", "⇧ z"],
> "action",
> () => $("li.logout button"),
> "inline",
> null,
> true,
> "Cerrar sesión"
> );
> 
> // ── Chat ──────────────────────────────────────────────────────────────────────
> 
> hint(
> ["-"],
> "action",
> () => $(".chat-header-icon"),
> "abs-br",
> null,
> false,
> "Alternar chat"
> );
> 
> // Alt+↑/↓, Alt+⇧↑/↓, ⇧Esc, Ctrl+K – panel añadido al cajón del chat
> hintPanel(
> "action",
> "inline",
> "dso-hint-chat-composer",
> () => $(".chat-drawer-content"),
> () =>
> buildPanel(
> "action",
> [
> { keys: ["Ctrl", "k"], label: "Canal rápido" },
> { keys: ["Alt", "↑ / ↓"], label: "Cambiar canal" },
> { keys: ["Alt", "⇧ ↑ / ↓"], label: "Cambiar sin leer" },
> { keys: ["⇧", "Esc"], label: "Marcar todo como leído" },
> ],
> {
> 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,
> "Cerrar chat"
> );
> 
> // ── Botón de alternancia ─────────────────────────────────────────────────────────────
> let visible = localStorage.getItem("dso-visible") === "true";
> 
> const toggleBtn = el("button", {
> id: "dso-toggle-btn",
> title: "Alternar superposición de atajos",
> 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] eliminado.", "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 activo.",
> "color:#6af;font-weight:bold"
> );
> })();
> 
> ```

Reemplaza o añade coincidencias de URL (`// @match https://meta.discourse.org/*`) si quieres usarlo en otro foro de Discourse.

Para instalar userscripts, usa una extensión de navegador: Greasemonkey, Tampermonkey, ScriptCat, o cualquier software externo que proporcione dicha funcionalidad, por ejemplo AdGuard.

---

<div class="post-metadata">

### Author: ![Moin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/moin/32/554653_2.png) [@Moin](https://meta.discourse.org/u/Moin)
#### Post date: [20 Febrero, 2026 12:13 UTC](https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588/2 "2026-02-20T12:13:34Z")

</div>

> [@Canapin](#):
>
> Barra lateral de filtros

Esa es para la barra lateral de administración

 ![image](https://global.discourse-cdn.com/meta/original/4X/6/1/8/6182c62463e978eea70eeb53b200dc6d09d15abc.png)

> [@Canapin](#):
>
> Descartar temas seleccionados

Eso es lo que puedes hacer aquí, en lugar de usar el botón de descartar en la parte superior

 ![image](https://global.discourse-cdn.com/meta/original/4X/1/d/4/1d4c70086b48dc600b9b26f5f55d8d1f072c0e6d.png)

---

<div class="post-metadata">

### Author: ![NateDhaliwal](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/natedhaliwal/32/313494_2.png) [@NateDhaliwal](https://meta.discourse.org/u/NateDhaliwal)
#### Post date: [20 Febrero, 2026 13:05 UTC](https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588/3 "2026-02-20T13:05:52Z")

</div>

¡Caramba! Solo con tu captura de pantalla, aprendí más de 15 atajos de teclado 👀 !

---

<div class="post-metadata">

### Author: ![Canapin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/canapin/32/119591_2.png) [@Canapin](https://meta.discourse.org/u/Canapin)
#### Post date: [22 Febrero, 2026 21:51 UTC](https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588/4 "2026-02-22T21:51:43Z")

</div>

He actualizado el script.

Añadí atajos que faltaban, especialmente en las acciones posteriores, hice los colores menos agresivos y agnósticos al tema (se integran bien en oscuro y claro).

Más importante aún, el script ahora recuerda su estado y no se reiniciará cuando recarguemos la página.

Lo he estado usando durante los últimos días, ¡y funciona muy bien para aprender los atajos!

Cómo me siento ahora usando Discourse:

![Realidad del Hacker: Escribiendo en un Teclado RGB](https://global.discourse-cdn.com/meta/original/4X/4/8/b/48bb02e693f88435f44f7c01a109b16489404203.webp)

---

<div class="post-metadata">

### Author: ![Canapin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/canapin/32/119591_2.png) [@Canapin](https://meta.discourse.org/u/Canapin)
#### Post date: [20 Abril, 2026 20:29 UTC](https://meta.discourse.org/t/shortcuts-overlay-a-userscript-to-help-you-learn-discourse-keyboard-shortcuts/396588/5 "2026-04-20T20:29:26Z")

</div>

Pequeña actualización:

- Se ha bajado el botón de alternar para que no se superponga al menú de avatar en temas de ancho completo:  

- Se ha corregido un error que impedía hacer clic en el logotipo de Discourse para ir a la página de inicio en el tema con marca meta.
