Add a copy button to highlighted code blocks in the documentation
that copies the full contents of the code block to the clipboard.
This is faster and less error-prone than manually selecting and
copying code from the page, especially for longer examples where
part of the block can be accidentally missed.
Keep the control hidden until the user interacts with the block so
it stays out of the way during normal reading. Reveal it on hover,
focus, and touch interaction, then copy the block contents to the
clipboard with a small success or failure state.
Signed-off-by: Rito Rhymes <rito@ritovision.com>
Assisted-by: Codex:GPT-5.4
Assisted-by: Claude Opus 4.6
---
Live demo:
https://kernel-docs-cp.ritovision.com/accounting/delay-accounting.html
I am willing to maintain this small feature and handle follow-up
fixes if problems come up. I do not expect it to expand
significantly beyond its current scope.
diff --git a/Documentation/conf.py b/Documentation/conf.py
index 679861503..ac63a3448 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -376,6 +376,7 @@ highlight_language = "none"
# Default theme
html_theme = "alabaster"
html_css_files = []
+html_js_files = ["copy-code.js"]
if "DOCS_THEME" in os.environ:
html_theme = os.environ["DOCS_THEME"]
diff --git a/Documentation/sphinx-static/copy-code.js b/Documentation/sphinx-static/copy-code.js
new file mode 100644
index 000000000..2684e9855
--- /dev/null
+++ b/Documentation/sphinx-static/copy-code.js
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: GPL-2.0
+
+(function () {
+ const BUTTON_LABEL = "Copy code";
+ const COPIED_LABEL = "Copied";
+ const FAILED_LABEL = "Copy failed";
+ const RESET_DELAY_MS = 2000;
+
+ const COPY_ICON = `
+ <svg viewBox="0 0 24 24" aria-hidden="true" fill="none"
+ stroke="currentColor" stroke-width="2"
+ stroke-linecap="round" stroke-linejoin="round">
+ <g transform="translate(24 0) scale(-1 1)">
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
+ </g>
+ </svg>`;
+
+ const COPIED_ICON = '<span aria-hidden="true">✓</span>';
+ const FAILED_ICON = '<span aria-hidden="true">×</span>';
+
+ function resetButtonState(button, status) {
+ button.dataset.copyState = "idle";
+ button.setAttribute("aria-label", BUTTON_LABEL);
+ button.setAttribute("title", BUTTON_LABEL);
+ button.innerHTML = COPY_ICON;
+ status.textContent = "";
+ }
+
+ function setButtonState(button, status, state, label, icon) {
+ button.dataset.copyState = state;
+ button.setAttribute("aria-label", label);
+ button.setAttribute("title", label);
+ button.innerHTML = icon;
+ status.textContent = label;
+
+ if (button.resetTimer) {
+ window.clearTimeout(button.resetTimer);
+ }
+
+ button.resetTimer = window.setTimeout(function () {
+ resetButtonState(button, status);
+ }, RESET_DELAY_MS);
+ }
+
+ async function copyText(text) {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch (error) {
+ /* Fall back to execCommand below. */
+ }
+ }
+
+ /* Fall back for browsers where the async clipboard API is unavailable. */
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("readonly", "");
+ textarea.style.position = "fixed";
+ textarea.style.left = "-9999px";
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ try {
+ return document.execCommand("copy");
+ } catch (error) {
+ return false;
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+
+ function hideVisibleButtons(exceptWrapper) {
+ document
+ .querySelectorAll("div.highlight.kernel-copy-visible")
+ .forEach(function (wrapper) {
+ if (wrapper !== exceptWrapper) {
+ wrapper.classList.remove("kernel-copy-visible");
+ }
+ });
+ }
+
+ function addCopyButton(wrapper) {
+ const pre = wrapper.querySelector("pre");
+
+ if (!pre || wrapper.querySelector(":scope > button.kernel-copy-button")) {
+ return;
+ }
+
+ const button = document.createElement("button");
+ const status = document.createElement("span");
+
+ button.className = "kernel-copy-button";
+ button.type = "button";
+ button.innerHTML = COPY_ICON;
+ resetButtonState(button, status);
+
+ status.className = "kernel-visually-hidden";
+ status.setAttribute("aria-live", "polite");
+ status.setAttribute("aria-atomic", "true");
+
+ button.addEventListener("click", async function () {
+ const ok = await copyText(pre.textContent || "");
+
+ if (ok) {
+ setButtonState(button, status, "copied", COPIED_LABEL, COPIED_ICON);
+ } else {
+ setButtonState(button, status, "error", FAILED_LABEL, FAILED_ICON);
+ }
+ });
+
+ wrapper.appendChild(button);
+ wrapper.appendChild(status);
+ wrapper.classList.add("kernel-copy-block");
+ }
+
+ function initCopyButtons() {
+ document.querySelectorAll("div.highlight").forEach(addCopyButton);
+
+ document.addEventListener("pointerdown", function (event) {
+ /* Hover already handles mouse users; this is for touch-style reveal. */
+ if (event.pointerType === "mouse") {
+ return;
+ }
+
+ const wrapper = event.target.closest("div.highlight.kernel-copy-block");
+ hideVisibleButtons(wrapper);
+
+ if (wrapper) {
+ wrapper.classList.add("kernel-copy-visible");
+ }
+ });
+
+ document.addEventListener("keydown", function (event) {
+ if (event.key === "Escape") {
+ hideVisibleButtons(null);
+ }
+ });
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initCopyButtons, { once: true });
+ } else {
+ initCopyButtons();
+ }
+})();
diff --git a/Documentation/sphinx-static/custom.css b/Documentation/sphinx-static/custom.css
index db24f4344..57c6d5327 100644
--- a/Documentation/sphinx-static/custom.css
+++ b/Documentation/sphinx-static/custom.css
@@ -169,3 +169,76 @@ a.manpage {
font-weight: bold;
font-family: "Courier New", Courier, monospace;
}
+
+/* Copy button for code blocks */
+/* Anchor the copy button to the highlighted code block. */
+div.highlight {
+ position: relative;
+}
+
+/* Hide the control until interaction so it stays out of the way and
+ * cannot be clicked while invisible.
+ */
+button.kernel-copy-button {
+ align-items: center;
+ background: #fafafa;
+ border: 1px solid #cccccc;
+ border-radius: 4px;
+ color: #333333;
+ cursor: pointer;
+ display: inline-flex;
+ height: 2rem;
+ justify-content: center;
+ opacity: 0;
+ padding: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0.5rem;
+ top: 0.5rem;
+ transition: opacity 0.12s ease-in-out, color 0.12s ease-in-out,
+ border-color 0.12s ease-in-out,
+ background-color 0.12s ease-in-out;
+ width: 2rem;
+ z-index: 1;
+}
+
+/* Reveal on hover/focus, or when JS marks the block visible for touch. */
+div.highlight:hover > button.kernel-copy-button,
+div.highlight:focus-within > button.kernel-copy-button,
+div.highlight.kernel-copy-visible > button.kernel-copy-button {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+button.kernel-copy-button:hover,
+button.kernel-copy-button:focus-visible {
+ background: #eeeeee;
+ border-color: #cccccc;
+ color: #333333;
+}
+
+button.kernel-copy-button svg {
+ height: 1rem;
+ width: 1rem;
+}
+
+button.kernel-copy-button span {
+ font-size: 1.2rem;
+ font-weight: bold;
+ line-height: 1;
+}
+
+/* Keep live status text available to assistive technology without
+ * showing it visually.
+ */
+.kernel-visually-hidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
diff --git a/MAINTAINERS b/MAINTAINERS
index 96ea84948..84b0cdd39 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7652,6 +7652,12 @@ X: Documentation/power/
X: Documentation/spi/
X: Documentation/userspace-api/media/
+DOCUMENTATION COPY CODE BUTTON
+R: Rito Rhymes <rito@ritovision.com>
+L: linux-doc@vger.kernel.org
+S: Supported
+F: Documentation/sphinx-static/copy-code.js
+
DOCUMENTATION PROCESS
M: Jonathan Corbet <corbet@lwn.net>
R: Shuah Khan <skhan@linuxfoundation.org>
--
2.51.0
Rito Rhymes <rito@ritovision.com> writes: > Add a copy button to highlighted code blocks in the documentation > that copies the full contents of the code block to the clipboard. > > This is faster and less error-prone than manually selecting and > copying code from the page, especially for longer examples where > part of the block can be accidentally missed. > > Keep the control hidden until the user interacts with the block so > it stays out of the way during normal reading. Reveal it on hover, > focus, and touch interaction, then copy the block contents to the > clipboard with a small success or failure state. > > Signed-off-by: Rito Rhymes <rito@ritovision.com> > Assisted-by: Codex:GPT-5.4 > Assisted-by: Claude Opus 4.6 > --- > Live demo: > https://kernel-docs-cp.ritovision.com/accounting/delay-accounting.html Honestly, I don't think so. Rito, who is asking for this feature? What is the use case? Does it really justify adding a blob of JavaScript code to every view of the kernel documentation - JavaScript that we will have to maintain going forward? Our goal here is to make the kernel documentation better, not to shovel lots of code into the repository. If you can get some acks from established kernel developers saying that they want this change, I will reconsider - but only after the merge window. But I really think that, again, this is something you might want to discuss with the Sphinx developers, then turn it into something without hard-coded colors that will work with whatever theme people might choose to build their docs with. jon
© 2016 - 2026 Red Hat, Inc.