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,43 @@
|
|||||||
|
name: Deploy website to GitHub Pages
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "site/**"
|
||||||
|
- "docs/**"
|
||||||
|
- "packaging/icons/ferese.svg"
|
||||||
|
- "scripts/build-site.py"
|
||||||
|
- ".github/workflows/pages.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/configure-pages@v5
|
||||||
|
- name: Build website and handbook
|
||||||
|
run: |
|
||||||
|
python3 -m venv .site-venv
|
||||||
|
.site-venv/bin/pip install -r site/requirements.txt
|
||||||
|
.site-venv/bin/python scripts/build-site.py
|
||||||
|
- uses: actions/upload-pages-artifact@v4
|
||||||
|
with:
|
||||||
|
path: build/site
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- name: Deploy
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -11,3 +11,8 @@ ChatGPT Image*.png
|
|||||||
/docs/ferese-window-manager-spec.md
|
/docs/ferese-window-manager-spec.md
|
||||||
/docs/ferese-theme-ui-spec.md
|
/docs/ferese-theme-ui-spec.md
|
||||||
/docs/ferese-shell-design.md
|
/docs/ferese-shell-design.md
|
||||||
|
|
||||||
|
# Generated GitHub Pages site
|
||||||
|
/build/site/
|
||||||
|
/.site-venv/
|
||||||
|
__pycache__/
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 397 KiB |
+17
-3
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
## Build and install
|
## Build and install
|
||||||
|
|
||||||
On Fedora with SDDM, install a Rust toolchain (1.93 or newer), native build
|
On Fedora with SDDM, install a Rust toolchain (1.95 or newer), native build
|
||||||
dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also
|
dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also
|
||||||
needs `zenity`; locking needs swaylock or swaylock-effects.
|
needs `zenity`; locking needs swaylock or swaylock-effects.
|
||||||
Screenshots need `grim`, `slurp`, `satty`, and `wl-clipboard`.
|
Screenshots need `grim`, `slurp`, `satty`, `wl-clipboard`, and Python 3.
|
||||||
|
Print Screen captures the active monitor; Super+Shift+S selects an area.
|
||||||
|
Use `ferese-screenshot --all` to capture all monitors in one image.
|
||||||
|
|
||||||
From the checkout, run as your normal user:
|
Clone the repository, then run the installer as your normal user:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
git clone https://github.com/borngraced/ferese.git
|
||||||
|
cd ferese
|
||||||
./scripts/install.sh
|
./scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -29,6 +33,16 @@ Use `--help` for all options. New configs can be copied from
|
|||||||
|
|
||||||
## Preview and logs
|
## Preview and logs
|
||||||
|
|
||||||
|
To preview a source build before installing, run these commands from the checkout
|
||||||
|
inside an existing Wayland desktop:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release --locked -p ferese -p ferese-shell -p ferese-settings -p feresectl
|
||||||
|
target/release/ferese --backend nested --grant-effects --grant-shell-control -- \
|
||||||
|
target/release/ferese-shell
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Run `ferese-session --nested` inside an existing Wayland desktop. For a hardware
|
Run `ferese-session --nested` inside an existing Wayland desktop. For a hardware
|
||||||
session, use the login screen or a spare TTY; never run Ferese as root or start
|
session, use the login screen or a spare TTY; never run Ferese as root or start
|
||||||
its DRM backend inside an active graphical session.
|
its DRM backend inside an active graphical session.
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ Cards appear beneath the bar on the focused output and follow the selected theme
|
|||||||
Click the notification icon in the bar to open history, toggle Do Not Disturb, or
|
Click the notification icon in the bar to open history, toggle Do Not Disturb, or
|
||||||
clear notifications. Escape or the close button closes the history panel.
|
clear notifications. Escape or the close button closes the history panel.
|
||||||
|
|
||||||
|
## Send a notification
|
||||||
|
|
||||||
Notifications use the standard `org.freedesktop.Notifications` D-Bus interface.
|
Notifications use the standard `org.freedesktop.Notifications` D-Bus interface.
|
||||||
Existing apps and `notify-send` work without Ferese-specific commands:
|
Existing apps and `notify-send` work without Ferese-specific commands:
|
||||||
|
|
||||||
@@ -12,6 +14,8 @@ Existing apps and `notify-send` work without Ferese-specific commands:
|
|||||||
notify-send --app-name=Ferese --icon=dialog-information "Hello" "Your desktop notification"
|
notify-send --app-name=Ferese --icon=dialog-information "Hello" "Your desktop notification"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Popups and history
|
||||||
|
|
||||||
Up to three popup cards are visible at once. History keeps the most recent 100
|
Up to three popup cards are visible at once. History keeps the most recent 100
|
||||||
notifications in memory and clears when the shell exits. Hovering a popup pauses
|
notifications in memory and clears when the shell exits. Hovering a popup pauses
|
||||||
its timer. Critical notifications bypass Do Not Disturb and do not expire unless
|
its timer. Critical notifications bypass Do Not Disturb and do not expire unless
|
||||||
@@ -19,6 +23,8 @@ the sender supplies a timeout. App action buttons emit the standard action signa
|
|||||||
a replacement updates the same notification ID. Transient messages are removed
|
a replacement updates the same notification ID. Transient messages are removed
|
||||||
from history when they close.
|
from history when they close.
|
||||||
|
|
||||||
|
## Settings and Do Not Disturb
|
||||||
|
|
||||||
Settings → Notifications controls popup visibility, the initial Do Not Disturb
|
Settings → Notifications controls popup visibility, the initial Do Not Disturb
|
||||||
state, and the default timeout. The bar's Do Not Disturb toggle is temporary;
|
state, and the default timeout. The bar's Do Not Disturb toggle is temporary;
|
||||||
changing the Settings option saves the preference for future sessions. Apps can
|
changing the Settings option saves the preference for future sessions. Apps can
|
||||||
@@ -35,6 +41,8 @@ notifications {
|
|||||||
The timeout must be between 1000 and 30000 milliseconds. Hiding popups still
|
The timeout must be between 1000 and 30000 milliseconds. Hiding popups still
|
||||||
records notifications in history. Appearance follows the Ferese theme.
|
records notifications in history. Appearance follows the Ferese theme.
|
||||||
|
|
||||||
|
## Session integration
|
||||||
|
|
||||||
Only one notification server can own the D-Bus interface. The Ferese session
|
Only one notification server can own the D-Bus interface. The Ferese session
|
||||||
launcher stops the SwayNotificationCenter user service before starting its shell.
|
launcher stops the SwayNotificationCenter user service before starting its shell.
|
||||||
For a manually started shell, stop that service with `systemctl --user stop
|
For a manually started shell, stop that service with `systemctl --user stop
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Capture an area or the whole desktop and open it in Satty for annotation.
|
# Capture an area, active monitor, or all monitors and edit in Satty.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
@@ -15,14 +15,15 @@ mode=area
|
|||||||
case ${1:-} in
|
case ${1:-} in
|
||||||
'') ;;
|
'') ;;
|
||||||
--full) mode=full; shift ;;
|
--full) mode=full; shift ;;
|
||||||
|
--all) mode=all; shift ;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo 'Usage: ferese-screenshot [--full]'
|
echo 'Usage: ferese-screenshot [--full | --all]'
|
||||||
echo 'Select an area, or use --full for the whole desktop, then edit in Satty.'
|
echo 'Select an area; --full captures the active monitor; --all captures all monitors.'
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*) fail 'Usage: ferese-screenshot [--full]' ;;
|
*) fail 'Usage: ferese-screenshot [--full | --all]' ;;
|
||||||
esac
|
esac
|
||||||
(($# == 0)) || fail 'Usage: ferese-screenshot [--full]'
|
(($# == 0)) || fail 'Usage: ferese-screenshot [--full | --all]'
|
||||||
|
|
||||||
for tool in grim satty wl-copy flock; do
|
for tool in grim satty wl-copy flock; do
|
||||||
command -v "$tool" >/dev/null || fail "Required command not found: $tool"
|
command -v "$tool" >/dev/null || fail "Required command not found: $tool"
|
||||||
@@ -38,6 +39,22 @@ if [[ $mode == area ]]; then
|
|||||||
geometry=$(slurp) || exit 0
|
geometry=$(slurp) || exit 0
|
||||||
[[ -n $geometry ]] || exit 0
|
[[ -n $geometry ]] || exit 0
|
||||||
capture_args=(-g "$geometry")
|
capture_args=(-g "$geometry")
|
||||||
|
elif [[ $mode == full ]]; then
|
||||||
|
for tool in feresectl python3; do
|
||||||
|
command -v "$tool" >/dev/null || fail "Required command not found: $tool"
|
||||||
|
done
|
||||||
|
# Resolve the active monitor before Satty can change keyboard focus.
|
||||||
|
# Never fall back to grim's all-monitor capture when IPC is unavailable.
|
||||||
|
output=$(feresectl get-outputs | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
outputs = [output for output in json.load(sys.stdin) if output.get("enabled")]
|
||||||
|
focused = [output for output in outputs if output.get("focused")]
|
||||||
|
selected = focused if len(focused) == 1 else outputs if len(outputs) == 1 else []
|
||||||
|
if len(selected) != 1 or not selected[0].get("name"):
|
||||||
|
sys.exit(1)
|
||||||
|
print(selected[0]["name"])
|
||||||
|
') || fail 'Could not determine the active monitor. Use area selection or --all.'
|
||||||
|
capture_args=(-o "$output")
|
||||||
fi
|
fi
|
||||||
pictures=$(xdg-user-dir PICTURES 2>/dev/null) || pictures="$HOME/Pictures"
|
pictures=$(xdg-user-dir PICTURES 2>/dev/null) || pictures="$HOME/Pictures"
|
||||||
directory=${pictures:-$HOME/Pictures}/Screenshots
|
directory=${pictures:-$HOME/Pictures}/Screenshots
|
||||||
@@ -48,13 +65,14 @@ trap '[[ -z $input ]] || rm -f -- "$input"; [[ -s $file ]] || rm -f -- "$file"'
|
|||||||
input=$(mktemp "$XDG_RUNTIME_DIR/ferese-screenshot-XXXXXX.png") || fail 'Could not create the temporary capture.'
|
input=$(mktemp "$XDG_RUNTIME_DIR/ferese-screenshot-XXXXXX.png") || fail 'Could not create the temporary capture.'
|
||||||
grim "${capture_args[@]}" "$input" || fail 'Could not capture the screen. Log back into Ferese after enabling screen capture.'
|
grim "${capture_args[@]}" "$input" || fail 'Could not capture the screen. Log back into Ferese after enabling screen capture.'
|
||||||
# Enter saves the annotated image and copies it; Escape discards the capture.
|
# Enter saves the annotated image and copies it; Escape discards the capture.
|
||||||
exec 9>&-
|
# Keep the parent lock through editing. Close it only in Satty so clipboard
|
||||||
|
# helpers cannot inherit the lock and keep later screenshots blocked.
|
||||||
satty --filename "$input" --output-filename "$file" \
|
satty --filename "$input" --output-filename "$file" \
|
||||||
--fullscreen current-screen \
|
--fullscreen current-screen \
|
||||||
--copy-command 'wl-copy --type image/png' \
|
--copy-command 'wl-copy --type image/png' \
|
||||||
--save-after-copy \
|
--save-after-copy \
|
||||||
--actions-on-enter save-to-file,save-to-clipboard,exit \
|
--actions-on-enter save-to-file,save-to-clipboard,exit \
|
||||||
--actions-on-escape exit || fail 'Could not open the screenshot in Satty.'
|
--actions-on-escape exit 9>&- || fail 'Could not open the screenshot in Satty.'
|
||||||
if [[ -s $file ]]; then
|
if [[ -s $file ]]; then
|
||||||
printf '%s\n' "$file"
|
printf '%s\n' "$file"
|
||||||
fi
|
fi
|
||||||
@@ -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()
|
||||||
@@ -500,7 +500,8 @@ impl cosmic::Application for App {
|
|||||||
.push(self.label("Ferese", 16.))
|
.push(self.label("Ferese", 16.))
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
.on_drag(Message::DragWindow),
|
.on_press(Message::DragWindow)
|
||||||
|
.interaction(cosmic::iced::mouse::Interaction::Grab),
|
||||||
)
|
)
|
||||||
.push(widget::Space::new().height(14))
|
.push(widget::Space::new().height(14))
|
||||||
.push(
|
.push(
|
||||||
@@ -865,7 +866,11 @@ impl cosmic::Application for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut content = column([]).spacing(16).push(heading);
|
let mut content = column([]).spacing(16).push(
|
||||||
|
widget::mouse_area(heading.width(Length::Fill))
|
||||||
|
.on_press(Message::DragWindow)
|
||||||
|
.interaction(cosmic::iced::mouse::Interaction::Grab),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(error) = &self.error {
|
if let Some(error) = &self.error {
|
||||||
content = content.push(
|
content = content.push(
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Ferese website and handbook
|
||||||
|
|
||||||
|
The public site is static HTML and CSS. The handbook is generated from the
|
||||||
|
project's Markdown guides, so website and repository documentation stay in sync.
|
||||||
|
There are no client frameworks, external fonts, or tracking scripts.
|
||||||
|
|
||||||
|
## Local preview
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -m venv .site-venv
|
||||||
|
.site-venv/bin/pip install -r site/requirements.txt
|
||||||
|
.site-venv/bin/python scripts/build-site.py
|
||||||
|
python3 -m http.server 8080 --bind 127.0.0.1 --directory build/site
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:8080 or http://localhost:8080/docs/. Rebuild after editing
|
||||||
|
site sources or Markdown guides, then refresh the browser.
|
||||||
|
|
||||||
|
- `site/index.html` and `site/styles.css`: homepage and shared visual design.
|
||||||
|
- `site/content/index.md`: handbook introduction.
|
||||||
|
- `docs/{installation,configuration,desktop-widgets,notifications}.md`: source guides.
|
||||||
|
- `scripts/build-site.py`: renders Markdown, resolves links, generates section
|
||||||
|
navigation, previous/next links, and the search index.
|
||||||
|
- `site/app.js`: handbook search and code copying. Reading and navigation work
|
||||||
|
without JavaScript. Search is local to the browser, with no third-party service.
|
||||||
|
|
||||||
|
## GitHub Pages
|
||||||
|
|
||||||
|
Select **Settings → Pages → Source → GitHub Actions**. The Pages workflow
|
||||||
|
publishes site and documentation changes pushed to `main`, or can run manually.
|
||||||
|
Expected URL: https://borngraced.github.io/ferese/.
|
||||||
|
|
||||||
|
Local links are relative, including documentation search, so publishing under
|
||||||
|
the `/ferese/` project path works. Screenshots are copied from `docs/images`
|
||||||
|
during the build; the generated `build/site` directory is ignored by Git.
|
||||||
|
|
||||||
|
See [GitHub's custom workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages)
|
||||||
|
for deployment setup.
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
// Progressive enhancement for the handbook. Navigation and content work without JS.
|
||||||
|
const search = document.querySelector("#docs-search");
|
||||||
|
const results = document.querySelector("#search-results");
|
||||||
|
const status = document.querySelector("#search-status");
|
||||||
|
let indexPromise;
|
||||||
|
if (search) {
|
||||||
|
search.closest(".docs-search").hidden = false;
|
||||||
|
const loadIndex = () =>
|
||||||
|
(indexPromise ||= fetch("search-index.json")
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error("Search index unavailable");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
indexPromise = null;
|
||||||
|
throw error;
|
||||||
|
}));
|
||||||
|
search.addEventListener("focus", () => {
|
||||||
|
loadIndex().catch(() => {});
|
||||||
|
});
|
||||||
|
search.addEventListener("input", async () => {
|
||||||
|
const query = search.value.trim().toLocaleLowerCase();
|
||||||
|
if (!query) {
|
||||||
|
results.hidden = true;
|
||||||
|
results.replaceChildren();
|
||||||
|
status.textContent = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const index = await loadIndex();
|
||||||
|
if (search.value.trim().toLocaleLowerCase() !== query) return;
|
||||||
|
const words = query.split(/\s+/);
|
||||||
|
const matches = index
|
||||||
|
.filter((item) =>
|
||||||
|
words.every((word) =>
|
||||||
|
`${item.page} ${item.heading} ${item.text}`
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(word),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Number(b.heading.toLocaleLowerCase().includes(query)) -
|
||||||
|
Number(a.heading.toLocaleLowerCase().includes(query)),
|
||||||
|
)
|
||||||
|
.slice(0, 10);
|
||||||
|
results.replaceChildren();
|
||||||
|
for (const item of matches) {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = item.url;
|
||||||
|
const title = document.createElement("strong");
|
||||||
|
title.textContent = `${item.page} / ${item.heading}`;
|
||||||
|
const snippet = document.createElement("span");
|
||||||
|
const position = Math.max(
|
||||||
|
0,
|
||||||
|
item.text.toLocaleLowerCase().indexOf(words[0]) - 45,
|
||||||
|
);
|
||||||
|
snippet.textContent = `${position ? "…" : ""}${item.text.slice(position, position + 165)}${item.text.length > position + 165 ? "…" : ""}`;
|
||||||
|
link.append(title, snippet);
|
||||||
|
results.append(link);
|
||||||
|
}
|
||||||
|
if (!matches.length) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
empty.textContent =
|
||||||
|
"No matching sections. Try “wallpaper”, “shortcuts”, or “notifications”.";
|
||||||
|
results.append(empty);
|
||||||
|
}
|
||||||
|
results.hidden = false;
|
||||||
|
status.textContent = `${matches.length} matching sections.`;
|
||||||
|
} catch {
|
||||||
|
if (search.value.trim().toLocaleLowerCase() !== query) return;
|
||||||
|
results.hidden = true;
|
||||||
|
status.textContent = "Search is unavailable. Browse the guides below.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
results.hidden = true;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.key === "/" &&
|
||||||
|
!["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey &&
|
||||||
|
!event.altKey
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.key === "ArrowDown" &&
|
||||||
|
document.activeElement === search &&
|
||||||
|
!results.hidden
|
||||||
|
) {
|
||||||
|
const first = results.querySelector("a");
|
||||||
|
if (first) {
|
||||||
|
event.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!event.target.closest(".docs-search")) results.hidden = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
for (const block of document.querySelectorAll(".prose pre")) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "code-copy";
|
||||||
|
button.textContent = "Copy";
|
||||||
|
button.setAttribute("aria-label", "Copy code example");
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
block.querySelector("code").textContent,
|
||||||
|
);
|
||||||
|
button.textContent = "Copied";
|
||||||
|
if (status) status.textContent = "Code example copied.";
|
||||||
|
} catch {
|
||||||
|
button.textContent = "Select to copy";
|
||||||
|
if (status)
|
||||||
|
status.textContent =
|
||||||
|
"Copy failed. Select the code to copy it manually.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
block.append(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# The Ferese handbook
|
||||||
|
|
||||||
|
Ferese is a complete Wayland desktop built around personalization. Its custom shell, native Settings, live themes, and flexible layouts work together as one cohesive experience.
|
||||||
|
|
||||||
|
This handbook covers installing Ferese, finding your way around, and making the desktop your own.
|
||||||
|
|
||||||
|
## Start with your first session
|
||||||
|
|
||||||
|
1. Read [Installation](installation.md) for the supported setup, building, and recovery.
|
||||||
|
2. Open **Control Center → Settings** to choose a theme and configure your desktop.
|
||||||
|
3. Explore the [configuration reference](configuration.md) for layouts, shortcuts, window rules, and displays.
|
||||||
|
|
||||||
|
Want to try it before switching desktops? The [preview instructions](installation.md#preview-and-logs) cover running Ferese inside an existing Wayland session.
|
||||||
|
|
||||||
|
## Find your way around
|
||||||
|
|
||||||
|
Ferese supports scrolling columns, tree tiling, floating windows, and a workspace overview. These are the built-in keyboard shortcuts; all can be changed in Settings.
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| Super + Enter | Open a terminal |
|
||||||
|
| Super + H / J / K / L | Focus a window |
|
||||||
|
| Super + R | Cycle column width |
|
||||||
|
| Super + Tab | Open workspace and window overview |
|
||||||
|
| Super + M | Switch scrolling / tree layout |
|
||||||
|
| Super + Shift + S | Select a screenshot area |
|
||||||
|
|
||||||
|
Three-finger swipes move between workspaces and windows. Open **Control Center → Settings → Shortcuts** to change the defaults or assign gestures to other actions.
|
||||||
|
|
||||||
|
## Make it feel like yours
|
||||||
|
|
||||||
|
Use **Settings** for everyday customization. Choose an appearance preset, change the wallpaper, adjust shell and window corners, or add a clock and sticky notes through [Desktop widgets](desktop-widgets.md).
|
||||||
|
|
||||||
|
[Notifications](notifications.md) explains popup cards, history, Do Not Disturb, and app actions.
|
||||||
|
|
||||||
|
If you prefer a text editor, your configuration lives at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/ferese/config.kdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Ferese reloads changes as you make them. An invalid edit keeps the last working configuration. Settings also preserves a backup of your previous save. See [Saving and validation](configuration.md#saving-and-validation).
|
||||||
|
|
||||||
|
## Keep a recovery path
|
||||||
|
|
||||||
|
Keep another desktop session available while testing hardware behavior. Save your work before logging out: `feresectl exit` and **Super + Shift + E** end the current session immediately.
|
||||||
|
|
||||||
|
The [recovery guide](installation.md#logout-and-recovery) explains logs, installed releases, and rollback.
|
||||||
|
|
||||||
|
## Work on Ferese
|
||||||
|
|
||||||
|
Ferese is built in Rust on Smithay. Use release builds when evaluating motion and startup performance. From the repository checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test --workspace --locked
|
||||||
|
cargo fmt --all --check
|
||||||
|
```
|
||||||
|
|
||||||
|
The [source repository](https://github.com/borngraced/ferese) is the place to explore implementation details and report issues. When reporting a problem, include how to reproduce it, whether you used the nested or DRM backend, and relevant logs.
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
<!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" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Ferese is a beautiful, fluid Wayland desktop made to feel like yours. Explore the desktop, then get started with the documentation."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:title"
|
||||||
|
content="Ferese — A desktop that feels like yours"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="A complete Wayland desktop built around personalization."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://borngraced.github.io/ferese/assets/ferese-desktop.png"
|
||||||
|
/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<title>Ferese — A desktop that feels like yours</title>
|
||||||
|
<link rel="icon" href="assets/ferese.svg" type="image/svg+xml" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<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="./" aria-current="page">Overview</a
|
||||||
|
><a href="docs/">Documentation</a
|
||||||
|
><a href="https://github.com/borngraced/ferese"
|
||||||
|
>GitHub <span aria-hidden="true">↗</span></a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main id="main" class="simple-home">
|
||||||
|
<section class="product-intro page-width">
|
||||||
|
<h1>Ferese</h1>
|
||||||
|
<p class="product-tagline">
|
||||||
|
A beautiful, fluid Wayland desktop made to feel like yours.
|
||||||
|
</p>
|
||||||
|
<p class="product-description">
|
||||||
|
A complete Linux desktop with a custom shell, native settings,
|
||||||
|
scrolling and tiling layouts, and live-reloading themes.
|
||||||
|
</p>
|
||||||
|
<div class="product-links">
|
||||||
|
<a class="button" href="docs/installation.html">Install Ferese</a>
|
||||||
|
<a class="button secondary" href="docs/">Documentation</a>
|
||||||
|
<a class="source-link" href="https://github.com/borngraced/ferese"
|
||||||
|
>Source code ↗</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<figure class="product-screenshot page-width">
|
||||||
|
<a
|
||||||
|
href="assets/ferese-desktop.png"
|
||||||
|
aria-label="View the full-resolution Ferese desktop screenshot"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="assets/ferese-desktop.png"
|
||||||
|
width="2880"
|
||||||
|
height="1800"
|
||||||
|
alt="Ferese Blue desktop with the default wallpaper, Settings, Control Center and clock widget"
|
||||||
|
fetchpriority="high"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<figcaption>
|
||||||
|
Ferese Blue with Settings, Control Center, and the default wallpaper.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
<section class="product-features page-width" aria-label="Features">
|
||||||
|
<article>
|
||||||
|
<h2>Flexible layouts</h2>
|
||||||
|
<p>
|
||||||
|
Scrolling columns, tree tiling, floating windows, and a workspace
|
||||||
|
overview. Use keyboard shortcuts or gestures to move around.
|
||||||
|
</p>
|
||||||
|
<a href="docs/configuration.html#layout">Configure layouts →</a>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h2>A native desktop</h2>
|
||||||
|
<p>
|
||||||
|
A menu bar, Control Center, Settings, notifications, and desktop
|
||||||
|
widgets, designed to work together.
|
||||||
|
</p>
|
||||||
|
<a href="docs/">Explore the handbook →</a>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h2>Make it yours</h2>
|
||||||
|
<p>
|
||||||
|
Choose your theme, wallpaper, colors, and motion. Change settings
|
||||||
|
through the app or a KDL config, with live reload.
|
||||||
|
</p>
|
||||||
|
<a href="docs/configuration.html#appearance">Customize Ferese →</a>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<section class="product-getting-started page-width">
|
||||||
|
<h2>Getting started</h2>
|
||||||
|
<p>
|
||||||
|
The <a href="docs/installation.html">installation guide</a> covers
|
||||||
|
Fedora with SDDM. You can also
|
||||||
|
<a href="docs/installation.html#preview-and-logs">preview Ferese</a>
|
||||||
|
inside an existing Wayland session before installing.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Built in Rust on Smithay. Development happens
|
||||||
|
<a href="https://github.com/borngraced/ferese">on GitHub</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="site-footer page-width">
|
||||||
|
<a class="wordmark" href="./">ferese</a>
|
||||||
|
<p>A beautiful, fluid Wayland desktop.</p>
|
||||||
|
<a href="docs/">Read the handbook <span aria-hidden="true">↗</span></a>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
markdown-it-py==3.0.0
|
||||||
|
mdurl==0.1.2
|
||||||
+1141
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user