diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index b3a5f12c2..c17c911d4 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -34,3 +34,62 @@ details > summary {
.highlight .w {
text-decoration: none
}
+
+/* Custom shader demo cards */
+.shader-demo-card {
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+.shader-demo-card:hover {
+ transform: translateY(-2px);
+}
+
+/* Video modal overlay */
+.shader-modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.85);
+ z-index: 10000;
+ align-items: center;
+ justify-content: center;
+}
+
+.shader-modal-overlay.active {
+ display: flex;
+}
+
+body.shader-modal-open {
+ overflow: hidden;
+}
+
+.shader-modal-container {
+ position: relative;
+ max-width: 90vw;
+ max-height: 90vh;
+}
+
+.shader-modal-close {
+ position: absolute;
+ top: -2rem;
+ right: 0;
+ background: transparent;
+ border: none;
+ color: #fff;
+ font-size: 2rem;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0;
+ opacity: 0.8;
+}
+
+.shader-modal-close:hover {
+ opacity: 1;
+}
+
+.shader-modal-video {
+ display: block;
+ max-width: 90vw;
+ max-height: 85vh;
+ border-radius: 4px;
+}
diff --git a/docs/_static/custom.js b/docs/_static/custom.js
index 87021c61c..14e68047a 100644
--- a/docs/_static/custom.js
+++ b/docs/_static/custom.js
@@ -60,5 +60,59 @@ function init_sidebar() {
document.addEventListener("DOMContentLoaded", init_sidebar);
init_sidebar();
+function init_shader_modals() {
+ var cards = document.querySelectorAll('.shader-demo-card');
+ if (!cards.length) return;
+
+ var overlay = document.createElement('div');
+ overlay.id = 'shader-video-modal';
+ overlay.className = 'shader-modal-overlay';
+ overlay.innerHTML =
+ '
' +
+ '' +
+ '' +
+ '
';
+ document.body.appendChild(overlay);
+
+ var video = overlay.querySelector('.shader-modal-video');
+ var close_btn = overlay.querySelector('.shader-modal-close');
+
+ function open_modal(video_src) {
+ video.innerHTML = '';
+ video.load();
+ video.play();
+ overlay.classList.add('active');
+ document.body.classList.add('shader-modal-open');
+ }
+
+ function close_modal() {
+ overlay.classList.remove('active');
+ video.pause();
+ video.innerHTML = '';
+ document.body.classList.remove('shader-modal-open');
+ }
+
+ cards.forEach(function(card) {
+ var link = card.querySelector('a.sd-stretched-link');
+ if (!link) return;
+ var href = link.getAttribute('href');
+ if (!href || href.slice(-5) !== '.webm') return;
+ link.addEventListener('click', function(e) {
+ e.preventDefault();
+ open_modal(href);
+ });
+ });
+
+ close_btn.addEventListener('click', close_modal);
+ overlay.addEventListener('click', function(e) {
+ if (e.target === overlay) close_modal();
+ });
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') close_modal();
+ });
+}
+
+document.addEventListener("DOMContentLoaded", init_shader_modals);
+
}());
diff --git a/docs/conf.py b/docs/conf.py
index 10f800a24..5d824e1eb 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -162,6 +162,7 @@ html_theme_options: Dict[str, Any] = {
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
+html_extra_path = ['screenshots']
html_favicon = html_logo = '../logo/kitty.svg'
html_css_files = ['custom.css', 'timestamps.css']
html_js_files = ['custom.js', 'timestamps.js']
@@ -308,6 +309,46 @@ if you specify a program-to-run you can use the special placeholder
# }}}
+def write_custom_shader_docs() -> None: # {{{
+ import kitty.shaders.custom.demo as demo_module
+
+ category_slug_map = {
+ 'cursor-trail': 'cursor-trails',
+ 'background': 'backgrounds',
+ 'mouse': 'mouse',
+ }
+
+ by_category: Dict[str, List[str]] = {}
+ for name, meta in demo_module.metadata.items():
+ cat_raw = str(meta.get('category', 'other'))
+ cat = category_slug_map.get(cat_raw, cat_raw)
+ by_category.setdefault(cat, []).append(name)
+
+ def shader_title(name: str) -> str:
+ return ' '.join(w.capitalize() for w in name.replace('-', ' ').split())
+
+ for category, shader_names in by_category.items():
+ lines: List[str] = []
+ lines.append('.. grid:: 1 2 2 3')
+ lines.append(' :gutter: 3')
+ lines.append('')
+ for shader_name in shader_names:
+ title = shader_title(shader_name)
+ video_url = f'{shader_name}.webm'
+ lines.append(f' .. grid-item-card:: {title}')
+ lines.append(f' :link: {video_url}')
+ lines.append(f' :link-type: url')
+ lines.append(f' :class-card: shader-demo-card')
+ lines.append('')
+ lines.append(f' Preview of the {shader_name} shader effect.')
+ lines.append('')
+ with open(f'generated/custom-shaders-{category}', 'w') as f:
+ f.write('\n'.join(lines))
+
+
+# }}}
+
+
def write_color_names_table() -> None: # {{{
from kitty.fast_data_types import all_color_names
@@ -785,6 +826,7 @@ def setup(app: Any) -> None:
write_cli_docs(kn)
write_remote_control_protocol_docs()
write_color_names_table()
+ write_custom_shader_docs()
write_conf_docs(app, kn)
app.connect('source-read', replace_string)
app.add_config_value('analytics_id', '', 'env')