Add dark website and documentation with GitHub Pages deployment
Include the updated desktop screenshot, active-monitor screenshot capture and editor locking, and Settings window drag areas.
This commit is contained in:
1 parent
e5e2b74a25
commit
0cd48bcc81
15 files changed
+1794
-12
No files matched your search
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Ferese's static website and handbook from the project Markdown docs."""
|
||||
from pathlib import Path
|
||||
from html import escape
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from urllib.parse import urlsplit
|
||||
from markdown_it import MarkdownIt
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OUTPUT = ROOT / 'build' / 'site'
|
||||
REPO = 'https://github.com/borngraced/ferese'
|
||||
PAGES = [
|
||||
('index', 'Introduction', ROOT / 'site/content/index.md'),
|
||||
('installation', 'Installation', ROOT / 'docs/installation.md'),
|
||||
('configuration', 'Configuration', ROOT / 'docs/configuration.md'),
|
||||
('desktop-widgets', 'Desktop widgets', ROOT / 'docs/desktop-widgets.md'),
|
||||
('notifications', 'Notifications', ROOT / 'docs/notifications.md'),
|
||||
]
|
||||
md = MarkdownIt('commonmark', {'html': False}).enable('table')
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
for name in ('index.html', 'styles.css', 'app.js'):
|
||||
shutil.copy2(ROOT / 'site' / name, OUTPUT / name)
|
||||
(OUTPUT / 'assets').mkdir(exist_ok=True)
|
||||
for name in ('ferese-desktop.png', 'ferese-monochrome.png'):
|
||||
shutil.copy2(ROOT / 'docs/images' / name, OUTPUT / 'assets' / name)
|
||||
shutil.copy2(ROOT / 'packaging/icons/ferese.svg', OUTPUT / 'assets/ferese.svg')
|
||||
(OUTPUT / 'docs').mkdir(exist_ok=True)
|
||||
(OUTPUT / '.nojekyll').touch()
|
||||
search_index = []
|
||||
|
||||
|
||||
def rewrite_link(href, source):
|
||||
url = urlsplit(href)
|
||||
if url.scheme or url.netloc or not url.path:
|
||||
return href
|
||||
basename = Path(url.path).stem
|
||||
if url.path.endswith('.md') and basename in {page[0] for page in PAGES}:
|
||||
return f'{basename}.html' + (f'#{url.fragment}' if url.fragment else '')
|
||||
destination = (source.parent / url.path).resolve().relative_to(ROOT)
|
||||
return f'{REPO}/blob/main/{destination}' + (f'#{url.fragment}' if url.fragment else '')
|
||||
|
||||
|
||||
for page_number, (slug, title, source) in enumerate(PAGES):
|
||||
tokens = md.parse(source.read_text())
|
||||
toc, ids, sections = [], set(), []
|
||||
current = None
|
||||
for i, token in enumerate(tokens):
|
||||
if token.type == 'heading_open':
|
||||
heading = tokens[i + 1].content
|
||||
base = re.sub(r'[^\w\s-]', '', heading.lower()).strip()
|
||||
base = re.sub(r'[\s_]+', '-', base)
|
||||
anchor = base
|
||||
suffix = 1
|
||||
while anchor in ids:
|
||||
anchor = f'{base}-{suffix}'
|
||||
suffix += 1
|
||||
ids.add(anchor)
|
||||
token.attrSet('id', anchor)
|
||||
if token.tag != 'h1':
|
||||
toc.append((anchor, heading))
|
||||
current = {'page': title, 'heading': heading, 'url': f'{slug}.html#{anchor}', 'text': ''}
|
||||
sections.append(current)
|
||||
elif current and token.type in ('inline', 'fence', 'code_block'):
|
||||
current['text'] += token.content + ' '
|
||||
for child in token.children or []:
|
||||
if child.type == 'link_open':
|
||||
child.attrSet('href', rewrite_link(child.attrGet('href'), source))
|
||||
search_index.extend(sections)
|
||||
body = md.renderer.render(tokens, md.options, {})
|
||||
body = body.replace('<table>', '<div class="table-scroll" tabindex="0" role="region" aria-label="Reference table"><table>').replace('</table>', '</table></div>')
|
||||
navigation = ''.join(f'<a href="{s}.html"' + (' aria-current="page"' if s == slug else '') + f'>{escape(t)}</a>' for s, t, _ in PAGES)
|
||||
contents = ''.join(f'<a href="#{anchor}">{escape(heading)}</a>' for anchor, heading in toc)
|
||||
pager = ''
|
||||
if page_number:
|
||||
prev = PAGES[page_number - 1]
|
||||
pager += f'<a href="{prev[0]}.html"><small>PREVIOUS</small>← {escape(prev[1])}</a>'
|
||||
if page_number + 1 < len(PAGES):
|
||||
nxt = PAGES[page_number + 1]
|
||||
pager += f'<a href="{nxt[0]}.html"><small>NEXT</small>{escape(nxt[1])} →</a>'
|
||||
doc = f'''<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#10151c"><title>{escape(title)} — Ferese handbook</title><meta name="description" content="{escape(title)} in the Ferese handbook: setup, configuration, and guides for your Wayland desktop.">
|
||||
<link rel="icon" href="../assets/ferese.svg" type="image/svg+xml"><link rel="stylesheet" href="../styles.css"><script src="../app.js" defer></script></head>
|
||||
<body class="docs-page"><a class="skip" href="#main">Skip to content</a>
|
||||
<header class="site-header"><div class="header-inner"><a class="wordmark" href="../" aria-label="Ferese home"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 3h6v18H3zM11 3h10v6H11zM11 11h7v6h-7z"/></svg>ferese</a><nav aria-label="Main navigation"><a href="../">Overview</a><a href="./" aria-current="page">Documentation</a><a href="{REPO}">GitHub ↗</a></nav></div></header>
|
||||
<div class="docs-layout"><aside class="docs-sidebar"><div class="docs-search" hidden><label class="sr-only" for="docs-search">Search the handbook</label><input id="docs-search" type="search" placeholder="Search the handbook…" autocomplete="off" aria-controls="search-results"><div class="search-results" id="search-results" hidden></div><p class="sr-only" id="search-status" role="status"></p></div><details open><summary>FERESE HANDBOOK</summary><nav aria-label="Documentation">{navigation}</nav></details><div class="sidebar-links"><a href="{REPO}/issues">Report an issue ↗</a><a href="../">Back to Ferese ←</a></div></aside>
|
||||
<main class="docs-article" id="main"><div class="breadcrumb"><a href="./">Handbook</a><span aria-hidden="true">/</span><span>{escape(title)}</span></div><article class="prose">{body}</article><a class="edit-link" href="{REPO}/blob/main/{source.relative_to(ROOT)}">View this guide on GitHub ↗</a><nav class="docs-pager" aria-label="Previous and next guide">{pager}</nav></main>
|
||||
<aside class="docs-toc" aria-label="On this page">{'<p>ON THIS PAGE</p><nav>' + contents + '</nav>' if contents else ''}</aside></div>
|
||||
<footer class="site-footer page-width"><a class="wordmark" href="../">ferese</a><p>A beautiful, fluid Wayland desktop.</p><a href="{REPO}">Built in the open ↗</a></footer></body></html>'''
|
||||
(OUTPUT / 'docs' / f'{slug}.html').write_text(doc)
|
||||
(OUTPUT / 'docs/search-index.json').write_text(json.dumps(search_index, ensure_ascii=False))
|
||||
print(f'Built website and {len(PAGES)} handbook pages at {OUTPUT}')
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise screenshot selection and locking without a Wayland desktop."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[2] / 'packaging/ferese-screenshot'
|
||||
|
||||
|
||||
class ScreenshotTest(unittest.TestCase):
|
||||
def test_active_output_all_outputs_and_editor_lock(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
bins = root / 'bin'
|
||||
bins.mkdir()
|
||||
env = dict(os.environ, XDG_RUNTIME_DIR=directory, TEST_ROOT=directory,
|
||||
PATH=f'{bins}:{os.environ["PATH"]}')
|
||||
(root / 'outputs.json').write_text(json.dumps([
|
||||
{'name': 'eDP-1', 'enabled': True, 'focused': False},
|
||||
{'name': 'HDMI-A-1', 'enabled': True, 'focused': True},
|
||||
]))
|
||||
scripts = {
|
||||
'grim': '''#!/bin/bash
|
||||
printf '%s\\n' "$@" >> "$TEST_ROOT/captures"
|
||||
printf image > "${@: -1}"
|
||||
''',
|
||||
'feresectl': '#!/bin/bash\ncat "$TEST_ROOT/outputs.json"\n',
|
||||
'notify-send': '#!/bin/bash\nexit 0\n',
|
||||
'wl-copy': '#!/bin/bash\nexit 0\n',
|
||||
'xdg-user-dir': '#!/bin/bash\nprintf "%s" "$TEST_ROOT/Pictures"\n',
|
||||
'satty': '''#!/bin/bash
|
||||
if [[ -e /proc/$$/fd/9 ]]; then touch "$TEST_ROOT/inherited-lock"; fi
|
||||
printf 'editor\\n' >> "$TEST_ROOT/editors"
|
||||
touch "$TEST_ROOT/ready"
|
||||
while [[ ! -e "$TEST_ROOT/release" ]]; do sleep 0.02; done
|
||||
''',
|
||||
}
|
||||
for name, contents in scripts.items():
|
||||
path = bins / name
|
||||
path.write_text(contents)
|
||||
path.chmod(0o755)
|
||||
def run(mode):
|
||||
return subprocess.run(['bash', str(SCRIPT), mode], env=env,
|
||||
capture_output=True, timeout=3)
|
||||
first = subprocess.Popen(['bash', str(SCRIPT), '--full'], env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
deadline = time.monotonic() + 5
|
||||
while not (root / 'ready').exists():
|
||||
if time.monotonic() > deadline or first.poll() is not None:
|
||||
self.fail('First screenshot did not open its editor')
|
||||
time.sleep(0.02)
|
||||
second = run('--full')
|
||||
self.assertEqual(second.returncode, 0, second.stderr)
|
||||
self.assertEqual((root / 'editors').read_text().splitlines(), ['editor'])
|
||||
self.assertFalse((root / 'inherited-lock').exists())
|
||||
(root / 'release').touch()
|
||||
_, stderr = first.communicate(timeout=3)
|
||||
self.assertEqual(first.returncode, 0, stderr)
|
||||
third = run('--full')
|
||||
self.assertEqual(third.returncode, 0, third.stderr)
|
||||
self.assertEqual((root / 'editors').read_text().splitlines(), ['editor', 'editor'])
|
||||
self.assertFalse(list((root / 'Pictures/Screenshots').glob('*.png')))
|
||||
self.assertFalse(list(root.glob('ferese-screenshot-*.png')))
|
||||
captures = (root / 'captures').read_text().splitlines()
|
||||
self.assertEqual(captures[0:2], ['-o', 'HDMI-A-1'])
|
||||
self.assertEqual(captures[3:5], ['-o', 'HDMI-A-1'])
|
||||
all_monitors = run('--all')
|
||||
self.assertEqual(all_monitors.returncode, 0, all_monitors.stderr)
|
||||
captures = (root / 'captures').read_text().splitlines()
|
||||
self.assertEqual(len(captures), 7) # --all passes only the filename
|
||||
for outputs in [[], [{'enabled': True, 'name': 'A'}, {'enabled': True, 'name': 'B'}]]:
|
||||
(root / 'outputs.json').write_text(json.dumps(outputs))
|
||||
unavailable = run('--full')
|
||||
self.assertNotEqual(unavailable.returncode, 0)
|
||||
self.assertIn(b'Could not determine the active monitor', unavailable.stderr)
|
||||
self.assertEqual((root / 'captures').read_text().splitlines(), captures)
|
||||
(root / 'outputs.json').write_text(json.dumps([
|
||||
{'name': 'eDP-1', 'enabled': False, 'focused': True},
|
||||
{'name': 'HDMI-A-1', 'enabled': True, 'focused': False},
|
||||
]))
|
||||
single = run('--full')
|
||||
self.assertEqual(single.returncode, 0, single.stderr)
|
||||
self.assertEqual((root / 'captures').read_text().splitlines()[-3:-1], ['-o', 'HDMI-A-1'])
|
||||
finally:
|
||||
(root / 'release').touch()
|
||||
if first.poll() is None:
|
||||
first.terminate()
|
||||
first.communicate(timeout=3)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user