feat: add screenshot and ocr functionality with web interface

build: update makefile with new dependencies for tesseract and libpng
chore: reorganize example files into examples directory
This commit is contained in:
retoor 2026-01-24 16:17:47 +01:00
parent 8d3dfcbc7e
commit f4af2b1f1e
42 changed files with 9207 additions and 79 deletions

View File

@ -5,7 +5,7 @@
CC = gcc
CFLAGS = -Wall -Wextra -Wpedantic -Wshadow -O2 -I./include
CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2
LDFLAGS = -lX11 -lXext -lXinerama -lXrandr -lXft -lfontconfig -ldbus-1 -lcurl -lm -lpthread
LDFLAGS = -lX11 -lXext -lXinerama -lXrandr -lXft -lfontconfig -ldbus-1 -lcurl -lm -lpthread -lXtst
# Directories
SRC_DIR = src
@ -28,8 +28,8 @@ DATADIR = $(PREFIX)/share
SYSCONFDIR = /etc
# Get pkg-config flags (with error handling)
PKG_CFLAGS := $(shell pkg-config --cflags x11 xext xinerama xrandr xft fontconfig dbus-1 2>/dev/null)
PKG_LIBS := $(shell pkg-config --libs x11 xext xinerama xrandr xft fontconfig dbus-1 2>/dev/null)
PKG_CFLAGS := $(shell pkg-config --cflags x11 xext xinerama xrandr xft fontconfig dbus-1 xtst libpng tesseract lept 2>/dev/null)
PKG_LIBS := $(shell pkg-config --libs x11 xext xinerama xrandr xft fontconfig dbus-1 xtst libpng tesseract lept 2>/dev/null)
# Use pkg-config if available
ifneq ($(PKG_LIBS),)
@ -128,7 +128,7 @@ $(BIN_DIR):
install: $(TARGET)
@echo "Installing DWN..."
@install -Dm755 $(TARGET) $(DESTDIR)$(BINDIR)/dwn
@install -Dm644 scripts/dwn.desktop $(DESTDIR)$(DATADIR)/xsessions/dwn.desktop
@install -Dm644 examples/dwn.desktop $(DESTDIR)$(DATADIR)/xsessions/dwn.desktop
@mkdir -p $(DESTDIR)$(SYSCONFDIR)/dwn
@install -Dm644 config/config.example $(DESTDIR)$(SYSCONFDIR)/dwn/config.example
@echo ""
@ -204,9 +204,15 @@ deps:
libxinerama-dev \
libxrandr-dev \
libxft-dev \
libxtst-dev \
libfontconfig1-dev \
libdbus-1-dev \
libcurl4-openssl-dev \
libpng-dev \
libtesseract-dev \
libleptonica-dev \
tesseract-ocr \
tesseract-ocr-eng \
xserver-xephyr \
dmenu; \
echo ""; \
@ -220,8 +226,13 @@ deps:
libXext-devel \
libXinerama-devel \
libXrandr-devel \
libXtst-devel \
dbus-devel \
libcurl-devel \
libpng-devel \
tesseract-devel \
leptonica-devel \
tesseract-langpack-eng \
xorg-x11-server-Xephyr \
dmenu; \
echo ""; \
@ -235,8 +246,13 @@ deps:
libxext \
libxinerama \
libxrandr \
libxtst \
dbus \
curl \
libpng \
tesseract \
tesseract-data-eng \
leptonica \
xorg-server-xephyr \
dmenu; \
echo ""; \
@ -247,7 +263,7 @@ deps:
echo "Please install these packages manually:"; \
echo " - GCC and Make"; \
echo " - pkg-config"; \
echo " - X11, Xext, Xinerama, Xrandr development libraries"; \
echo " - X11, Xext, Xinerama, Xrandr, Xtst development libraries"; \
echo " - D-Bus development library"; \
echo " - libcurl development library"; \
echo " - Xephyr (for testing)"; \

BIN
bin/dwn

Binary file not shown.

View File

@ -19,7 +19,7 @@ build/client.o: src/client.c include/client.h include/dwn.h \
/usr/include/dbus-1.0/dbus/dbus-signature.h \
/usr/include/dbus-1.0/dbus/dbus-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/layout.h \
include/panel.h
include/panel.h include/api.h
include/client.h:
include/dwn.h:
include/atoms.h:
@ -48,3 +48,4 @@ include/notifications.h:
/usr/include/dbus-1.0/dbus/dbus-threads.h:
include/layout.h:
include/panel.h:
include/api.h:

Binary file not shown.

View File

@ -19,7 +19,7 @@ build/keys.o: src/keys.c include/keys.h include/dwn.h include/client.h \
/usr/include/dbus-1.0/dbus/dbus-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/news.h \
include/applauncher.h include/decorations.h include/demo.h \
include/layout.h
include/layout.h include/api.h
include/keys.h:
include/dwn.h:
include/client.h:
@ -51,3 +51,4 @@ include/applauncher.h:
include/decorations.h:
include/demo.h:
include/layout.h:
include/api.h:

Binary file not shown.

View File

@ -20,7 +20,8 @@ build/main.o: src/main.c include/dwn.h include/config.h include/dwn.h \
/usr/include/dbus-1.0/dbus/dbus-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/systray.h \
include/news.h include/applauncher.h include/ai.h include/autostart.h \
include/services.h include/api.h include/demo.h include/util.h
include/services.h include/api.h include/demo.h include/screenshot.h \
include/ocr.h include/util.h
include/dwn.h:
include/config.h:
include/dwn.h:
@ -58,4 +59,6 @@ include/autostart.h:
include/services.h:
include/api.h:
include/demo.h:
include/screenshot.h:
include/ocr.h:
include/util.h:

Binary file not shown.

View File

@ -17,7 +17,7 @@ build/notifications.o: src/notifications.c include/notifications.h \
/usr/include/dbus-1.0/dbus/dbus-signature.h \
/usr/include/dbus-1.0/dbus/dbus-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/config.h \
include/util.h
include/util.h include/api.h
include/notifications.h:
include/dwn.h:
/usr/include/dbus-1.0/dbus/dbus.h:
@ -40,3 +40,4 @@ include/dwn.h:
/usr/include/dbus-1.0/dbus/dbus-threads.h:
include/config.h:
include/util.h:
include/api.h:

Binary file not shown.

View File

@ -1,6 +1,6 @@
build/workspace.o: src/workspace.c include/workspace.h include/dwn.h \
include/client.h include/layout.h include/atoms.h include/util.h \
include/config.h include/panel.h
include/config.h include/panel.h include/api.h
include/workspace.h:
include/dwn.h:
include/client.h:
@ -9,3 +9,4 @@ include/atoms.h:
include/util.h:
include/config.h:
include/panel.h:
include/api.h:

Binary file not shown.

View File

@ -18,16 +18,15 @@
<body>
<div class="container py-5">
<h1 class="mb-4 text-primary">DWN Remote</h1>
<div class="row">
<div class="col-md-4">
<div class="card p-3">
<h3>Workspaces</h3>
<div id="workspace-grid" class="d-flex flex-wrap">
<!-- Workspaces injected here -->
</div>
</div>
<div class="card p-3">
<h3>Quick Launch</h3>
<div class="input-group mb-3">
@ -35,13 +34,32 @@
<button class="btn btn-outline-primary" onclick="launchCmd()">Run</button>
</div>
</div>
<div class="card p-3">
<h3>Screenshot</h3>
<div class="d-flex flex-wrap gap-2 mb-3">
<button class="btn btn-outline-info" onclick="takeScreenshot('fullscreen')">Fullscreen</button>
<button class="btn btn-outline-info" onclick="takeScreenshot('active')">Active Window</button>
</div>
<div id="screenshot-container" style="display:none;">
<img id="screenshot-img" class="img-fluid rounded" style="max-height: 300px;">
<div class="mt-2">
<small id="screenshot-info" class="text-muted"></small>
<button class="btn btn-sm btn-outline-success ms-2" onclick="downloadScreenshot()">Download</button>
<button class="btn btn-sm btn-outline-warning ms-2" onclick="runOCR()">OCR</button>
</div>
</div>
<div id="ocr-result" class="mt-3" style="display:none;">
<h6>OCR Result <small class="text-muted" id="ocr-confidence"></small></h6>
<pre class="bg-dark p-2 rounded" style="max-height:150px;overflow:auto;"><code id="ocr-text"></code></pre>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card p-3">
<h3>Active Windows</h3>
<div id="client-list">
<!-- Clients injected here -->
</div>
</div>
</div>
@ -50,6 +68,7 @@
<script>
let ws;
let lastScreenshotData = null;
const port = 8777;
const uri = `ws://${window.location.hostname || 'localhost'}:${port}/ws`;
@ -61,18 +80,20 @@
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (Array.isArray(data)) {
if (data.format === 'png' && data.encoding === 'base64') {
handleScreenshotResponse(data);
} else if (data.text !== undefined && data.confidence !== undefined) {
handleOCRResponse(data);
} else if (Array.isArray(data)) {
if (data.length > 0 && 'title' in data[0]) renderClients(data);
else if (data.length > 0 && 'layout' in data[0]) renderWorkspaces(data);
} else if (data.status === 'ok') {
// Refresh status if needed
}
};
ws.onclose = () => setTimeout(connect, 2000);
}
function refresh() {
if (ws.readyState === WebSocket.OPEN) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({command: "get_workspaces"}));
ws.send(JSON.stringify({command: "get_clients"}));
}
@ -122,6 +143,44 @@
}
}
function takeScreenshot(mode) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({command: "screenshot", mode: mode}));
}
}
function handleScreenshotResponse(data) {
if (data.status === 'ok' && data.data) {
lastScreenshotData = data.data;
const img = document.getElementById('screenshot-img');
img.src = 'data:image/png;base64,' + data.data;
document.getElementById('screenshot-container').style.display = 'block';
document.getElementById('screenshot-info').textContent = `${data.width}x${data.height}`;
document.getElementById('ocr-result').style.display = 'none';
}
}
function downloadScreenshot() {
if (!lastScreenshotData) return;
const link = document.createElement('a');
link.href = 'data:image/png;base64,' + lastScreenshotData;
link.download = 'screenshot.png';
link.click();
}
function runOCR() {
if (!lastScreenshotData || !ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({command: "ocr", image: lastScreenshotData, language: "eng"}));
}
function handleOCRResponse(data) {
if (data.status === 'ok') {
document.getElementById('ocr-result').style.display = 'block';
document.getElementById('ocr-confidence').textContent = `(${(data.confidence * 100).toFixed(1)}% confidence)`;
document.getElementById('ocr-text').textContent = data.text || '(No text detected)';
}
}
connect();
setInterval(refresh, 3000);
</script>

View File

@ -1,8 +1,8 @@
#!/bin/sh
# retoor <retoor@molodetz.nl>
# Example .xinitrc for starting DWN with startx
# Copy this to ~/.xinitrc and modify as needed
# Set environment variables
export XDG_SESSION_TYPE=x11
export XDG_CURRENT_DESKTOP=DWN
@ -10,7 +10,6 @@ export XDG_CURRENT_DESKTOP=DWN
# export OPENROUTER_API_KEY="your-api-key-here"
# export EXA_API_KEY="your-exa-key-here"
# Optional: Start background services
# Start D-Bus session bus if not already running
if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then
eval $(dbus-launch --sh-syntax)
@ -22,9 +21,6 @@ fi
# Optional: Set wallpaper
# feh --bg-scale ~/.wallpaper.png &
# Optional: Start notification daemon (DWN has built-in)
# Note: DWN includes its own notification daemon
# Optional: Start XFCE components for additional functionality
# xfce4-power-manager &
# xfsettingsd &
@ -38,5 +34,4 @@ fi
# Optional: Start clipboard manager
# xfce4-clipman &
# Start DWN
exec dwn

View File

@ -1,16 +1,107 @@
/*
* retoor <retoor@molodetz.nl>
* DWN - Desktop Window Manager
* WebSocket API
* WebSocket API with event subscription system
*/
#ifndef DWN_API_H
#define DWN_API_H
#include <stdbool.h>
#include <X11/Xlib.h>
typedef enum {
EVENT_WINDOW_CREATED,
EVENT_WINDOW_DESTROYED,
EVENT_WINDOW_FOCUSED,
EVENT_WINDOW_UNFOCUSED,
EVENT_WINDOW_MOVED,
EVENT_WINDOW_RESIZED,
EVENT_WINDOW_MINIMIZED,
EVENT_WINDOW_RESTORED,
EVENT_WINDOW_MAXIMIZED,
EVENT_WINDOW_UNMAXIMIZED,
EVENT_WINDOW_FULLSCREEN,
EVENT_WINDOW_UNFULLSCREEN,
EVENT_WINDOW_FLOATING,
EVENT_WINDOW_UNFLOATING,
EVENT_WINDOW_TITLE_CHANGED,
EVENT_WINDOW_RAISED,
EVENT_WINDOW_LOWERED,
EVENT_WORKSPACE_SWITCHED,
EVENT_WORKSPACE_WINDOW_ADDED,
EVENT_WORKSPACE_WINDOW_REMOVED,
EVENT_WORKSPACE_LAYOUT_CHANGED,
EVENT_WORKSPACE_MASTER_RATIO_CHANGED,
EVENT_WORKSPACE_MASTER_COUNT_CHANGED,
EVENT_WORKSPACE_ARRANGED,
EVENT_LAYOUT_CHANGED,
EVENT_KEY_PRESSED,
EVENT_KEY_RELEASED,
EVENT_SHORTCUT_TRIGGERED,
EVENT_MOUSE_MOVED,
EVENT_MOUSE_BUTTON_PRESSED,
EVENT_MOUSE_BUTTON_RELEASED,
EVENT_DRAG_STARTED,
EVENT_DRAG_ENDED,
EVENT_SHOW_DESKTOP_TOGGLED,
EVENT_NOTIFICATION_SHOWN,
EVENT_NOTIFICATION_CLOSED,
EVENT_COUNT
} ApiEventType;
void api_init(void);
void api_process(void);
void api_cleanup(void);
void api_handle_json_command(const char *json_str);
const char *api_event_name(ApiEventType type);
void api_emit_window_created(Window window, const char *title, const char *class_name, int workspace);
void api_emit_window_destroyed(Window window, const char *title, int workspace);
void api_emit_window_focused(Window window, const char *title, Window prev_window);
void api_emit_window_unfocused(Window window, const char *title);
void api_emit_window_moved(Window window, int old_x, int old_y, int new_x, int new_y);
void api_emit_window_resized(Window window, int old_w, int old_h, int new_w, int new_h);
void api_emit_window_minimized(Window window);
void api_emit_window_restored(Window window);
void api_emit_window_maximized(Window window, bool maximized);
void api_emit_window_fullscreen(Window window, bool fullscreen);
void api_emit_window_floating(Window window, bool floating);
void api_emit_window_title_changed(Window window, const char *old_title, const char *new_title);
void api_emit_window_raised(Window window);
void api_emit_window_lowered(Window window);
void api_emit_workspace_switched(int old_workspace, int new_workspace);
void api_emit_workspace_window_added(int workspace, Window window);
void api_emit_workspace_window_removed(int workspace, Window window);
void api_emit_workspace_layout_changed(int workspace, int old_layout, int new_layout);
void api_emit_workspace_master_ratio_changed(int workspace, float old_ratio, float new_ratio);
void api_emit_workspace_master_count_changed(int workspace, int old_count, int new_count);
void api_emit_workspace_arranged(int workspace, int layout);
void api_emit_key_pressed(unsigned int keycode, unsigned int keysym, unsigned int modifiers);
void api_emit_key_released(unsigned int keycode, unsigned int keysym, unsigned int modifiers);
void api_emit_shortcut_triggered(const char *name, const char *description);
void api_emit_mouse_moved(int x, int y);
void api_emit_mouse_button_pressed(int button, int x, int y);
void api_emit_mouse_button_released(int button, int x, int y);
void api_emit_drag_started(Window window, bool is_resize);
void api_emit_drag_ended(Window window, bool is_resize);
void api_emit_show_desktop_toggled(bool shown);
void api_emit_notification_shown(unsigned int id, const char *summary, const char *body);
void api_emit_notification_closed(unsigned int id);
#endif

70
include/ocr.h Normal file
View File

@ -0,0 +1,70 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* OCR text extraction API
*/
#ifndef DWN_OCR_H
#define DWN_OCR_H
#include <stdbool.h>
#include <stddef.h>
#include <pthread.h>
typedef enum {
OCR_OK = 0,
OCR_ERROR_INVALID_ARG,
OCR_ERROR_INIT,
OCR_ERROR_IMAGE,
OCR_ERROR_NO_MEMORY,
OCR_ERROR_NOT_AVAILABLE
} OcrStatus;
typedef enum {
OCR_STATE_IDLE,
OCR_STATE_PENDING,
OCR_STATE_COMPLETED,
OCR_STATE_ERROR
} OcrState;
typedef struct {
char *text;
size_t length;
float confidence;
} OcrResult;
typedef struct OcrRequest {
unsigned char *png_data;
size_t png_size;
char language[16];
OcrResult result;
OcrStatus status;
OcrState state;
void (*callback)(struct OcrRequest *req);
void *user_data;
pthread_t thread;
struct OcrRequest *next;
} OcrRequest;
bool ocr_init(void);
void ocr_cleanup(void);
bool ocr_is_available(void);
OcrStatus ocr_extract_from_png(const unsigned char *png_data, size_t size,
const char *language, OcrResult *result);
OcrRequest *ocr_extract_async(const unsigned char *png_data, size_t size,
const char *language,
void (*callback)(OcrRequest *));
void ocr_process_pending(void);
void ocr_cancel(OcrRequest *req);
void ocr_result_free(OcrResult *result);
const char *ocr_status_string(OcrStatus status);
#endif

84
include/screenshot.h Normal file
View File

@ -0,0 +1,84 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* Screenshot capture API
*/
#ifndef DWN_SCREENSHOT_H
#define DWN_SCREENSHOT_H
#include <stdbool.h>
#include <stddef.h>
#include <pthread.h>
#include <X11/Xlib.h>
typedef enum {
SCREENSHOT_OK = 0,
SCREENSHOT_ERROR_INVALID_ARG,
SCREENSHOT_ERROR_X11,
SCREENSHOT_ERROR_PNG,
SCREENSHOT_ERROR_NO_MEMORY,
SCREENSHOT_ERROR_WINDOW_NOT_FOUND
} ScreenshotStatus;
typedef enum {
SCREENSHOT_STATE_IDLE,
SCREENSHOT_STATE_PENDING,
SCREENSHOT_STATE_COMPLETED,
SCREENSHOT_STATE_ERROR
} ScreenshotState;
typedef enum {
SCREENSHOT_MODE_FULLSCREEN,
SCREENSHOT_MODE_WINDOW,
SCREENSHOT_MODE_ACTIVE,
SCREENSHOT_MODE_AREA
} ScreenshotMode;
typedef struct {
unsigned char *data;
size_t size;
int width;
int height;
} ScreenshotResult;
typedef struct ScreenshotRequest {
ScreenshotMode mode;
Window window;
int x, y, width, height;
ScreenshotResult result;
ScreenshotStatus status;
ScreenshotState state;
void (*callback)(struct ScreenshotRequest *req);
void *user_data;
pthread_t thread;
struct ScreenshotRequest *next;
} ScreenshotRequest;
bool screenshot_init(void);
void screenshot_cleanup(void);
ScreenshotStatus screenshot_capture_fullscreen(ScreenshotResult *result);
ScreenshotStatus screenshot_capture_window(Window window, ScreenshotResult *result);
ScreenshotStatus screenshot_capture_active(ScreenshotResult *result);
ScreenshotStatus screenshot_capture_area(int x, int y, int width, int height, ScreenshotResult *result);
ScreenshotRequest *screenshot_capture_async(ScreenshotMode mode,
void (*callback)(ScreenshotRequest *));
ScreenshotRequest *screenshot_capture_window_async(Window window,
void (*callback)(ScreenshotRequest *));
ScreenshotRequest *screenshot_capture_area_async(int x, int y, int w, int h,
void (*callback)(ScreenshotRequest *));
void screenshot_process_pending(void);
void screenshot_cancel(ScreenshotRequest *req);
char *screenshot_to_base64(const ScreenshotResult *result, size_t *out_len);
void screenshot_result_free(ScreenshotResult *result);
const char *screenshot_status_string(ScreenshotStatus status);
#endif

338
manual/ai-features.html Normal file
View File

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Integration - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link active">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>AI Integration</h1>
<p class="lead">Setup and usage of AI-powered features</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#overview">Overview</a></li>
<li><a href="#setup">Setup</a></li>
<li><a href="#command-palette">AI Command Palette</a></li>
<li><a href="#context-analysis">Context Analysis</a></li>
<li><a href="#exa-search">Exa Semantic Search</a></li>
<li><a href="#configuration">Configuration</a></li>
</ul>
</div>
<h2 id="overview">Overview</h2>
<p>DWN includes optional AI integration for intelligent command execution and semantic web search. These features require API keys from external services.</p>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-title">AI Command Palette</div>
<div class="feature-desc">Natural language commands via OpenRouter API. Ask AI to launch apps, answer questions, or perform tasks.</div>
</div>
<div class="feature-card">
<div class="feature-title">Context Analysis</div>
<div class="feature-desc">AI analyzes your current workspace to understand what task you're working on.</div>
</div>
<div class="feature-card">
<div class="feature-title">Exa Search</div>
<div class="feature-desc">Semantic web search that understands meaning, not just keywords. Find relevant docs and tutorials.</div>
</div>
</div>
<h2 id="setup">Setup</h2>
<h3>OpenRouter API Key</h3>
<p>Required for AI Command Palette and Context Analysis.</p>
<ol>
<li>Visit <a href="https://openrouter.ai/keys" target="_blank">https://openrouter.ai/keys</a></li>
<li>Create an account and generate an API key</li>
<li>Set the key via environment variable or config file</li>
</ol>
<div class="code-block">
<pre><code># Option 1: Environment variable (add to ~/.bashrc or ~/.zshrc)
export OPENROUTER_API_KEY=sk-or-v1-your-key-here
# Option 2: Config file (~/.config/dwn/config)
[ai]
openrouter_api_key = sk-or-v1-your-key-here</code></pre>
</div>
<h3>Exa API Key</h3>
<p>Required for Exa Semantic Search.</p>
<ol>
<li>Visit <a href="https://dashboard.exa.ai/api-keys" target="_blank">https://dashboard.exa.ai/api-keys</a></li>
<li>Create an account and generate an API key</li>
<li>Set the key via environment variable or config file</li>
</ol>
<div class="code-block">
<pre><code># Option 1: Environment variable
export EXA_API_KEY=your-exa-key-here
# Option 2: Config file (~/.config/dwn/config)
[ai]
exa_api_key = your-exa-key-here</code></pre>
</div>
<div class="alert alert-info">
<strong>Note:</strong> AI features are optional. DWN functions fully without them.
</div>
<h2 id="command-palette">AI Command Palette</h2>
<p>Press <code>Super+Shift+A</code> to open the AI command palette.</p>
<h3>Usage</h3>
<ol>
<li>Press <code>Super+Shift+A</code></li>
<li>Type a natural language command</li>
<li>Press Enter</li>
<li>AI interprets and executes your request</li>
</ol>
<h3>Example Commands</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Command</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>"open firefox"</td>
<td>Launches Firefox browser</td>
</tr>
<tr>
<td>"launch terminal"</td>
<td>Opens configured terminal</td>
</tr>
<tr>
<td>"run file manager"</td>
<td>Opens configured file manager</td>
</tr>
<tr>
<td>"what time is it"</td>
<td>Shows current time</td>
</tr>
<tr>
<td>"how much memory is free"</td>
<td>Shows system memory info</td>
</tr>
</tbody>
</table>
</div>
<h3>Supported Actions</h3>
<ul>
<li><strong>Application launching</strong> - "open", "launch", "run", "start"</li>
<li><strong>System queries</strong> - Time, date, system information</li>
<li><strong>General questions</strong> - AI will provide helpful responses</li>
</ul>
<h2 id="context-analysis">Context Analysis</h2>
<p>Press <code>Super+A</code> to see AI analysis of your current workspace.</p>
<h3>What It Shows</h3>
<ul>
<li>Detected task type (coding, browsing, communication, etc.)</li>
<li>Currently focused window</li>
<li>AI-generated suggestions based on context</li>
</ul>
<h3>Example Output</h3>
<div class="code-block">
<pre><code>Current Task: Software Development
Focused: Visual Studio Code - project.py
Windows: VS Code, Terminal, Firefox (Stack Overflow)
Suggestions:
- Run tests with Ctrl+Shift+T
- Toggle terminal with Ctrl+`
- Search documentation with Super+Shift+E</code></pre>
</div>
<h2 id="exa-search">Exa Semantic Search</h2>
<p>Press <code>Super+Shift+E</code> to open semantic web search.</p>
<h3>How It Works</h3>
<p>Exa uses neural search to find content based on meaning rather than keyword matching. This produces more relevant results for technical queries.</p>
<h3>Usage</h3>
<ol>
<li>Press <code>Super+Shift+E</code></li>
<li>Type a natural language query</li>
<li>Press Enter</li>
<li>Results appear in a selection menu</li>
<li>Select a result to open in browser</li>
</ol>
<h3>Example Queries</h3>
<ul>
<li>"how to configure nginx reverse proxy"</li>
<li>"python async await tutorial"</li>
<li>"X11 window manager development guide"</li>
<li>"systemd service file examples"</li>
</ul>
<h3>Best Practices</h3>
<ul>
<li>Use natural language, not keywords</li>
<li>Be specific about what you're looking for</li>
<li>Include context (language, framework, etc.)</li>
</ul>
<h2 id="configuration">Configuration</h2>
<p>AI settings in <code>~/.config/dwn/config</code>:</p>
<div class="code-block">
<pre><code>[ai]
# AI model for OpenRouter (see https://openrouter.ai/models)
model = google/gemini-2.0-flash-exp:free
# API keys (can also use environment variables)
openrouter_api_key = sk-or-v1-your-key
exa_api_key = your-exa-key</code></pre>
</div>
<h3>Available Models</h3>
<p>OpenRouter provides access to many AI models. Some options:</p>
<div class="table-container">
<table>
<thead>
<tr>
<th>Model</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>google/gemini-2.0-flash-exp:free</code></td>
<td>Fast, free tier available (default)</td>
</tr>
<tr>
<td><code>anthropic/claude-3-haiku</code></td>
<td>Fast and efficient</td>
</tr>
<tr>
<td><code>openai/gpt-4o-mini</code></td>
<td>Good balance of speed and quality</td>
</tr>
<tr>
<td><code>x-ai/grok-code-fast-1</code></td>
<td>Optimized for code tasks</td>
</tr>
</tbody>
</table>
</div>
<p>See <a href="https://openrouter.ai/models" target="_blank">OpenRouter Models</a> for the full list.</p>
<h2>Keyboard Shortcuts</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+A</code></td>
<td>Show AI context analysis</td>
</tr>
<tr>
<td><code>Super+Shift+A</code></td>
<td>Open AI command palette</td>
</tr>
<tr>
<td><code>Super+Shift+E</code></td>
<td>Open Exa semantic search</td>
</tr>
</tbody>
</table>
</div>
<h2>Troubleshooting</h2>
<h3>AI features not working</h3>
<ol>
<li>Verify API key is set correctly</li>
<li>Check network connectivity</li>
<li>Look for errors in <code>~/.local/share/dwn/dwn.log</code></li>
</ol>
<h3>Slow responses</h3>
<ul>
<li>Try a faster model (e.g., <code>google/gemini-2.0-flash-exp:free</code>)</li>
<li>Check network latency to API endpoints</li>
</ul>
<h3>API rate limits</h3>
<p>Free tiers have usage limits. Consider upgrading for heavy use or use a paid model.</p>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

828
manual/api-examples.html Normal file
View File

@ -0,0 +1,828 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Examples - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link active">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>API Examples</h1>
<p class="lead">Code examples for common automation tasks</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#python">Python Examples</a></li>
<li><a href="#javascript">JavaScript Examples</a></li>
<li><a href="#bash">Bash Examples</a></li>
<li><a href="#events">Event Subscription</a></li>
<li><a href="#automation">Automation Recipes</a></li>
<li><a href="#browser-ocr">Browser Automation with OCR</a></li>
</ul>
</div>
<h2 id="python">Python Examples</h2>
<h3>Basic Connection</h3>
<div class="code-block">
<pre><code>import json
import websocket
ws = websocket.create_connection("ws://localhost:8777")
def send_command(command, **params):
request = {"command": command, **params}
ws.send(json.dumps(request))
return json.loads(ws.recv())
# List all windows
result = send_command("list_windows")
for window in result["windows"]:
print(f"{window['title']} ({window['class']})")
ws.close()</code></pre>
</div>
<h3>Using the Client Library</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
client = DWNClient()
client.connect()
# Get all windows
windows = client.list_windows()
print(f"Found {len(windows)} windows")
# Focus window by title
for w in windows:
if "Firefox" in w["title"]:
client.focus_window(w["id"])
break
# Switch to workspace 3
client.switch_workspace(2)
# Type some text
client.type_text("Hello from Python!")
client.disconnect()</code></pre>
</div>
<h3>Screenshot and OCR</h3>
<div class="code-block">
<pre><code>import base64
from dwn_api_client import DWNClient
client = DWNClient()
client.connect()
# Take fullscreen screenshot
result = client.screenshot("fullscreen")
# Save to file
png_data = base64.b64decode(result["data"])
with open("screenshot.png", "wb") as f:
f.write(png_data)
print(f"Saved {result['width']}x{result['height']} screenshot")
# Extract text with OCR
ocr_result = client.ocr(result["data"])
print(f"Extracted text (confidence: {ocr_result['confidence']:.0%}):")
print(ocr_result["text"])
client.disconnect()</code></pre>
</div>
<h3>Window Arrangement Script</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
def arrange_coding_setup(client):
"""Arrange windows for coding: editor left, terminal right"""
windows = client.list_windows()
# Find VS Code and terminal
vscode = None
terminal = None
for w in windows:
if "code" in w["class"].lower():
vscode = w
elif "terminal" in w["class"].lower():
terminal = w
if vscode:
client.move_window(vscode["id"], 0, 32)
client.resize_window(vscode["id"], 960, 1048)
if terminal:
client.move_window(terminal["id"], 960, 32)
client.resize_window(terminal["id"], 960, 1048)
client = DWNClient()
client.connect()
arrange_coding_setup(client)
client.disconnect()</code></pre>
</div>
<h3>Async Client</h3>
<div class="code-block">
<pre><code>import asyncio
import json
import websockets
async def main():
async with websockets.connect("ws://localhost:8777") as ws:
# Send command
await ws.send(json.dumps({"command": "list_windows"}))
# Receive response
response = json.loads(await ws.recv())
for window in response["windows"]:
print(f"Window: {window['title']}")
asyncio.run(main())</code></pre>
</div>
<h2 id="javascript">JavaScript Examples</h2>
<h3>Browser WebSocket</h3>
<div class="code-block">
<pre><code>class DWNClient {
constructor(url = 'ws://localhost:8777') {
this.url = url;
this.ws = null;
this.pending = new Map();
this.requestId = 0;
}
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => resolve();
this.ws.onerror = (e) => reject(e);
this.ws.onmessage = (e) => this.handleMessage(e);
});
}
handleMessage(event) {
const response = JSON.parse(event.data);
// Handle response
console.log('Received:', response);
}
send(command, params = {}) {
const request = { command, ...params };
this.ws.send(JSON.stringify(request));
}
async listWindows() {
this.send('list_windows');
}
async focusWindow(windowId) {
this.send('focus_window', { window: windowId });
}
async typeText(text) {
this.send('type_text', { text });
}
async screenshot(mode = 'fullscreen') {
this.send('screenshot', { mode });
}
}
// Usage
const client = new DWNClient();
await client.connect();
await client.listWindows();</code></pre>
</div>
<h3>Node.js Client</h3>
<div class="code-block">
<pre><code>const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8777');
ws.on('open', () => {
console.log('Connected to DWN');
// List windows
ws.send(JSON.stringify({ command: 'list_windows' }));
});
ws.on('message', (data) => {
const response = JSON.parse(data);
if (response.windows) {
response.windows.forEach(w => {
console.log(`${w.title} - ${w.class}`);
});
}
ws.close();
});
ws.on('error', (err) => {
console.error('Error:', err.message);
});</code></pre>
</div>
<h3>Screenshot to Canvas</h3>
<div class="code-block">
<pre><code>async function captureToCanvas(client, canvasId) {
return new Promise((resolve) => {
client.ws.onmessage = (event) => {
const response = JSON.parse(event.data);
if (response.data) {
const img = new Image();
img.onload = () => {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
canvas.width = response.width;
canvas.height = response.height;
ctx.drawImage(img, 0, 0);
resolve();
};
img.src = 'data:image/png;base64,' + response.data;
}
};
client.send('screenshot', { mode: 'fullscreen' });
});
}</code></pre>
</div>
<h2 id="bash">Bash Examples</h2>
<h3>Using websocat</h3>
<div class="code-block">
<pre><code>#!/bin/bash
# List windows
echo '{"command": "list_windows"}' | websocat ws://localhost:8777
# Focus window by ID
echo '{"command": "focus_window", "window": 12345678}' | websocat ws://localhost:8777
# Switch workspace
echo '{"command": "switch_workspace", "workspace": 2}' | websocat ws://localhost:8777
# Take screenshot and save
echo '{"command": "screenshot", "mode": "fullscreen"}' | \
websocat ws://localhost:8777 | \
jq -r '.data' | \
base64 -d > screenshot.png</code></pre>
</div>
<h3>Using wscat</h3>
<div class="code-block">
<pre><code>#!/bin/bash
# One-liner command
echo '{"command": "list_windows"}' | wscat -c ws://localhost:8777 -w 1
# Interactive session
wscat -c ws://localhost:8777
# Then type commands manually</code></pre>
</div>
<h3>Using curl with websocat</h3>
<div class="code-block">
<pre><code>#!/bin/bash
dwn_command() {
echo "$1" | websocat -n1 ws://localhost:8777
}
# Get focused window
dwn_command '{"command": "get_focused"}' | jq '.window.title'
# Type text
dwn_command '{"command": "type_text", "text": "Hello!"}'
# Launch application
dwn_command '{"command": "spawn", "program": "firefox"}'</code></pre>
</div>
<h2 id="events">Event Subscription</h2>
<h3>Using dwn_automation_demo.py</h3>
<p>The included automation demo script provides ready-to-use event monitoring:</p>
<div class="code-block">
<pre><code># List available events
python3 examples/dwn_automation_demo.py events --list
# Monitor all events (Ctrl+C to stop)
python3 examples/dwn_automation_demo.py events --monitor
# Monitor specific events
python3 examples/dwn_automation_demo.py events -e window_focused -e workspace_switched
# Run event demo (10 seconds)
python3 examples/dwn_automation_demo.py demo events
# Full event stream demo with summary
python3 examples/dwn_automation_demo.py demo events-all</code></pre>
</div>
<h3>Python Event Listener</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
def on_event(event):
event_name = event.get("event", "unknown")
data = event.get("data", {})
if event_name == "window_focused":
print(f"Focus: {data.get('title')}")
elif event_name == "workspace_switched":
print(f"Workspace: {data.get('new_workspace') + 1}")
elif event_name == "shortcut_triggered":
print(f"Shortcut: {data.get('description')}")
return True # Continue listening
client = DWNClient()
client.connect()
# Subscribe and listen
client.listen_events(on_event, events=[
"window_focused",
"workspace_switched",
"shortcut_triggered"
])</code></pre>
</div>
<h3>Window Activity Logger</h3>
<div class="code-block">
<pre><code>import json
from datetime import datetime
from dwn_api_client import DWNClient
def activity_logger():
client = DWNClient()
client.connect()
client.subscribe(events=[
"window_focused",
"window_created",
"window_destroyed"
])
print("Logging window activity (Ctrl+C to stop)...")
try:
with open("activity.log", "a") as f:
while True:
event = client.receive_event(timeout=1.0)
if event and event.get("type") == "event":
timestamp = datetime.now().isoformat()
ev_name = event.get("event")
data = event.get("data", {})
log_entry = {
"time": timestamp,
"event": ev_name,
"data": data
}
f.write(json.dumps(log_entry) + "\\n")
f.flush()
print(f"[{timestamp}] {ev_name}")
except KeyboardInterrupt:
print("\\nStopped logging")
finally:
client.disconnect()
activity_logger()</code></pre>
</div>
<h3>JavaScript Event Listener</h3>
<div class="code-block">
<pre><code>const ws = new WebSocket('ws://localhost:8777');
ws.onopen = () => {
console.log('Connected to DWN');
// Subscribe to events
ws.send(JSON.stringify({
command: 'subscribe',
events: ['window_focused', 'workspace_switched']
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'event') {
console.log(`Event: ${msg.event}`, msg.data);
// Handle specific events
switch (msg.event) {
case 'window_focused':
document.title = msg.data.title || 'DWN';
break;
case 'workspace_switched':
updateWorkspaceIndicator(msg.data.new_workspace);
break;
}
}
};
ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
ws.onclose = () => {
console.log('Disconnected from DWN');
};</code></pre>
</div>
<h3>Reactive Window Arrangement</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
RULES = {
"code": {"floating": False, "workspace": 0},
"firefox": {"floating": False, "workspace": 1},
"slack": {"floating": True, "workspace": 2},
"telegram": {"floating": True, "workspace": 2},
}
def auto_arrange():
client = DWNClient()
client.connect()
client.subscribe(events=["window_created"])
print("Auto-arranging windows...")
try:
while True:
event = client.receive_event(timeout=1.0)
if event and event.get("event") == "window_created":
data = event.get("data", {})
window_id = data.get("window")
wm_class = data.get("class", "").lower()
for pattern, rules in RULES.items():
if pattern in wm_class:
print(f"Applying rules to {wm_class}")
if "workspace" in rules:
client.move_client_to_workspace(
window_id, rules["workspace"]
)
if "floating" in rules:
client.float_client(
window_id, rules["floating"]
)
break
except KeyboardInterrupt:
pass
finally:
client.disconnect()
auto_arrange()</code></pre>
</div>
<h2 id="automation">Automation Recipes</h2>
<h3>Auto-Arrange Windows by Class</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
LAYOUT_RULES = {
"code": {"workspace": 0, "floating": False},
"firefox": {"workspace": 1, "floating": False},
"telegram": {"workspace": 2, "floating": True},
"slack": {"workspace": 2, "floating": True},
}
def auto_arrange():
client = DWNClient()
client.connect()
windows = client.list_windows()
for w in windows:
win_class = w["class"].lower()
for pattern, rules in LAYOUT_RULES.items():
if pattern in win_class:
if w["workspace"] != rules["workspace"]:
client.move_window_to_workspace(
w["id"], rules["workspace"]
)
if w["floating"] != rules["floating"]:
client.set_floating(w["id"], rules["floating"])
break
client.disconnect()
auto_arrange()</code></pre>
</div>
<h3>Screenshot Monitor</h3>
<div class="code-block">
<pre><code>import time
import base64
from datetime import datetime
from dwn_api_client import DWNClient
def screenshot_monitor(interval=60, output_dir="screenshots"):
"""Take periodic screenshots"""
import os
os.makedirs(output_dir, exist_ok=True)
client = DWNClient()
client.connect()
try:
while True:
result = client.screenshot("fullscreen")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{output_dir}/screen_{timestamp}.png"
with open(filename, "wb") as f:
f.write(base64.b64decode(result["data"]))
print(f"Saved: {filename}")
time.sleep(interval)
except KeyboardInterrupt:
print("Stopped")
finally:
client.disconnect()
screenshot_monitor(interval=300) # Every 5 minutes</code></pre>
</div>
<h3>Window Focus Logger</h3>
<div class="code-block">
<pre><code>import time
from datetime import datetime
from dwn_api_client import DWNClient
def focus_logger(log_file="focus_log.txt"):
"""Log window focus changes"""
client = DWNClient()
client.connect()
last_focused = None
try:
with open(log_file, "a") as f:
while True:
windows = client.list_windows()
focused = next((w for w in windows if w["focused"]), None)
if focused and focused["id"] != last_focused:
timestamp = datetime.now().isoformat()
entry = f"{timestamp} | {focused['title']} ({focused['class']})\n"
f.write(entry)
f.flush()
print(entry.strip())
last_focused = focused["id"]
time.sleep(1)
except KeyboardInterrupt:
print("Stopped")
finally:
client.disconnect()
focus_logger()</code></pre>
</div>
<h3>Keyboard Macro</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
import time
def run_macro(client, actions, delay=0.1):
"""Execute a sequence of actions"""
for action in actions:
if action["type"] == "key":
client.key_press(action["key"], action.get("modifiers", []))
elif action["type"] == "type":
client.type_text(action["text"])
elif action["type"] == "click":
client.mouse_click(action.get("button", 1),
action.get("x"), action.get("y"))
elif action["type"] == "wait":
time.sleep(action["seconds"])
time.sleep(delay)
# Example: Open terminal and run command
macro = [
{"type": "key", "key": "t", "modifiers": ["ctrl", "alt"]},
{"type": "wait", "seconds": 1},
{"type": "type", "text": "ls -la"},
{"type": "key", "key": "Return"},
]
client = DWNClient()
client.connect()
run_macro(client, macro)
client.disconnect()</code></pre>
</div>
<h3>OCR Screen Reader</h3>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
def read_active_window():
"""Extract and print text from active window"""
client = DWNClient()
client.connect()
# Capture active window
screenshot = client.screenshot("active")
# Extract text
ocr_result = client.ocr(screenshot["data"])
print(f"Text from active window (confidence: {ocr_result['confidence']:.0%}):")
print("-" * 40)
print(ocr_result["text"])
client.disconnect()
read_active_window()</code></pre>
</div>
<h3 id="browser-ocr">Browser Automation with OCR</h3>
<p>Complete example that opens a browser, performs a Google search, scrolls through results, and extracts text using OCR. See <code>examples/browser_ocr_demo.py</code> for the full script.</p>
<div class="code-block">
<pre><code>#!/usr/bin/env python3
import asyncio
import json
import sys
import base64
from typing import List
class DWNAutomation:
def __init__(self, host: str = "localhost", port: int = 8777):
self.uri = f"ws://{host}:{port}/ws"
self.ws = None
async def connect(self):
import websockets
self.ws = await websockets.connect(self.uri)
async def disconnect(self):
if self.ws:
await self.ws.close()
async def send_command(self, command: dict) -> dict:
await self.ws.send(json.dumps(command))
return json.loads(await self.ws.recv())
async def run_command(self, exec_cmd: str) -> dict:
return await self.send_command({"command": "run_command", "exec": exec_cmd})
async def get_focused_client(self) -> dict:
return await self.send_command({"command": "get_focused_client"})
async def focus_client(self, window_id: int) -> dict:
return await self.send_command({"command": "focus_client", "window": window_id})
async def key_tap(self, keysym: str, modifiers: List[str] = None) -> dict:
cmd = {"command": "key_tap", "keysym": keysym}
if modifiers:
cmd["modifiers"] = modifiers
return await self.send_command(cmd)
async def key_type(self, text: str) -> dict:
return await self.send_command({"command": "key_type", "text": text})
async def mouse_scroll(self, direction: str, amount: int = 1) -> dict:
return await self.send_command({
"command": "mouse_scroll", "direction": direction, "amount": amount
})
async def screenshot(self, mode: str = "active") -> dict:
return await self.send_command({"command": "screenshot", "mode": mode})
async def ocr(self, image_base64: str) -> dict:
return await self.send_command({"command": "ocr", "image": image_base64})
async def main():
automation = DWNAutomation()
await automation.connect()
# Open default browser with Google
await automation.run_command("xdg-open https://www.google.nl")
await asyncio.sleep(5.0)
# Get focused browser window
result = await automation.get_focused_client()
window_id = int(result.get("client", {}).get("window", 0))
# Search for something
await automation.key_type("ponies")
await automation.key_tap("Return")
await asyncio.sleep(4.0)
# Scroll and collect OCR text from multiple pages
all_text = []
for i in range(4):
# Take screenshot and run OCR
screenshot = await automation.screenshot("active")
image_data = screenshot.get("data", "")
# Save screenshot
with open(f"screenshot_{i+1}.png", "wb") as f:
f.write(base64.b64decode(image_data))
# Extract text
ocr_result = await automation.ocr(image_data)
text = ocr_result.get("text", "").strip()
if text:
all_text.append(f"--- Page {i+1} ---\n{text}")
# Scroll down for next page
if i < 3:
await automation.mouse_scroll("down", 5)
await asyncio.sleep(1.5)
# Print combined results
print("EXTRACTED TEXT:")
print("\n\n".join(all_text))
await automation.disconnect()
asyncio.run(main())</code></pre>
</div>
<p>Run the included demo script:</p>
<div class="code-block">
<pre><code># Install dependency
pip install websockets
# Run the demo
python3 examples/browser_ocr_demo.py</code></pre>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

313
manual/api-overview.html Normal file
View File

@ -0,0 +1,313 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Overview - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link active">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>API Overview</h1>
<p class="lead">Introduction to DWN's WebSocket API</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#introduction">Introduction</a></li>
<li><a href="#enabling">Enabling the API</a></li>
<li><a href="#connecting">Connecting</a></li>
<li><a href="#protocol">Protocol</a></li>
<li><a href="#quick-start">Quick Start</a></li>
<li><a href="#clients">Client Libraries</a></li>
</ul>
</div>
<h2 id="introduction">Introduction</h2>
<p>DWN provides a WebSocket API for full programmatic control of the window manager. This enables:</p>
<ul>
<li><strong>Test Automation</strong> - Automated UI testing and scripting</li>
<li><strong>Custom Hotkeys</strong> - Build custom keyboard shortcuts</li>
<li><strong>Remote Control</strong> - Control DWN from other machines or devices</li>
<li><strong>Integration</strong> - Connect DWN to other applications</li>
<li><strong>Accessibility</strong> - Build alternative input methods</li>
</ul>
<h2 id="enabling">Enabling the API</h2>
<p>The API is disabled by default. Enable it in <code>~/.config/dwn/config</code>:</p>
<div class="code-block">
<pre><code>[api]
enabled = true
port = 8777</code></pre>
</div>
<p>Restart DWN for changes to take effect.</p>
<div class="alert alert-info">
<strong>Security Note:</strong> The API listens on localhost only by default. Be cautious when exposing to network.
</div>
<h2 id="connecting">Connecting</h2>
<p>Connect via WebSocket to <code>ws://localhost:8777</code> (or your configured port).</p>
<h3>Testing with wscat</h3>
<div class="code-block">
<pre><code># Install wscat
npm install -g wscat
# Connect to DWN
wscat -c ws://localhost:8777
# Send a command
> {"command": "list_windows"}
< {"status": "ok", "windows": [...]}</code></pre>
</div>
<h3>Testing with websocat</h3>
<div class="code-block">
<pre><code># Install websocat
cargo install websocat
# Connect and send command
echo '{"command": "list_windows"}' | websocat ws://localhost:8777</code></pre>
</div>
<h2 id="protocol">Protocol</h2>
<h3>Request Format</h3>
<p>All requests are JSON objects with a <code>command</code> field:</p>
<div class="code-block">
<pre><code>{
"command": "command_name",
"param1": "value1",
"param2": "value2"
}</code></pre>
</div>
<h3>Response Format</h3>
<p>Responses include a <code>status</code> field:</p>
<div class="code-block">
<pre><code>// Success
{
"status": "ok",
"data": {...}
}
// Error
{
"status": "error",
"message": "Error description"
}</code></pre>
</div>
<h3>Command Categories</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Category</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
<tr>
<td>Windows</td>
<td>list_windows, focus_window, close_window, move_window, resize_window</td>
</tr>
<tr>
<td>Workspaces</td>
<td>list_workspaces, switch_workspace, move_to_workspace</td>
</tr>
<tr>
<td>Layout</td>
<td>get_layout, set_layout, set_master_ratio</td>
</tr>
<tr>
<td>Keyboard</td>
<td>key_press, key_release, type_text</td>
</tr>
<tr>
<td>Mouse</td>
<td>mouse_move, mouse_click, mouse_drag</td>
</tr>
<tr>
<td>Screenshot</td>
<td>screenshot</td>
</tr>
<tr>
<td>OCR</td>
<td>ocr</td>
</tr>
<tr>
<td>System</td>
<td>get_monitors, spawn, notify</td>
</tr>
</tbody>
</table>
</div>
<h2 id="quick-start">Quick Start</h2>
<h3>List Windows</h3>
<div class="code-block">
<pre><code>// Request
{"command": "list_windows"}
// Response
{
"status": "ok",
"windows": [
{
"id": 12345678,
"title": "Firefox",
"class": "firefox",
"workspace": 0,
"x": 0, "y": 32,
"width": 960, "height": 540,
"focused": true,
"floating": false
}
]
}</code></pre>
</div>
<h3>Focus a Window</h3>
<div class="code-block">
<pre><code>{"command": "focus_window", "window": 12345678}</code></pre>
</div>
<h3>Switch Workspace</h3>
<div class="code-block">
<pre><code>{"command": "switch_workspace", "workspace": 2}</code></pre>
</div>
<h3>Type Text</h3>
<div class="code-block">
<pre><code>{"command": "type_text", "text": "Hello, World!"}</code></pre>
</div>
<h3>Take Screenshot</h3>
<div class="code-block">
<pre><code>// Request
{"command": "screenshot", "mode": "fullscreen"}
// Response
{
"status": "ok",
"format": "png",
"encoding": "base64",
"width": 1920,
"height": 1080,
"data": "iVBORw0KGgo..."
}</code></pre>
</div>
<h2 id="clients">Client Libraries</h2>
<h3>Python Client</h3>
<p>A Python client is included in the <code>examples/</code> directory:</p>
<div class="code-block">
<pre><code>from dwn_api_client import DWNClient
# Connect
client = DWNClient()
client.connect()
# List windows
windows = client.list_windows()
# Focus a window
client.focus_window(windows[0]['id'])
# Take screenshot
result = client.screenshot('fullscreen')
with open('screenshot.png', 'wb') as f:
f.write(base64.b64decode(result['data']))
# Disconnect
client.disconnect()</code></pre>
</div>
<h3>JavaScript (Browser)</h3>
<div class="code-block">
<pre><code>const ws = new WebSocket('ws://localhost:8777');
ws.onopen = () => {
ws.send(JSON.stringify({command: 'list_windows'}));
};
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
console.log(response);
};</code></pre>
</div>
<h3>Web Remote</h3>
<p>A web-based remote control interface is included at <code>examples/web_remote.html</code>. Open it in a browser to control DWN graphically.</p>
<h2>Next Steps</h2>
<ul>
<li><a href="api-reference.html">API Reference</a> - Complete command documentation</li>
<li><a href="api-examples.html">API Examples</a> - Code examples for common tasks</li>
</ul>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

858
manual/api-reference.html Normal file
View File

@ -0,0 +1,858 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Reference - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link active">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>API Reference</h1>
<p class="lead">Complete WebSocket API command reference</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#windows">Window Commands</a></li>
<li><a href="#workspaces">Workspace Commands</a></li>
<li><a href="#layout">Layout Commands</a></li>
<li><a href="#keyboard">Keyboard Commands</a></li>
<li><a href="#mouse">Mouse Commands</a></li>
<li><a href="#screenshot">Screenshot Commands</a></li>
<li><a href="#ocr">OCR Commands</a></li>
<li><a href="#events">Event Subscription</a></li>
<li><a href="#system">System Commands</a></li>
</ul>
</div>
<h2 id="windows">Window Commands</h2>
<h3>list_windows</h3>
<p>Get list of all managed windows.</p>
<div class="code-block">
<pre><code>// Request
{"command": "list_windows"}
// Response
{
"status": "ok",
"windows": [
{
"id": 12345678,
"title": "Window Title",
"class": "window-class",
"workspace": 0,
"x": 0,
"y": 32,
"width": 960,
"height": 540,
"focused": true,
"floating": false,
"fullscreen": false,
"maximized": false,
"minimized": false
}
]
}</code></pre>
</div>
<h3>focus_window</h3>
<p>Focus a window by ID.</p>
<div class="code-block">
<pre><code>// Request
{"command": "focus_window", "window": 12345678}
// Response
{"status": "ok"}</code></pre>
</div>
<h3>close_window</h3>
<p>Close a window by ID.</p>
<div class="code-block">
<pre><code>// Request
{"command": "close_window", "window": 12345678}
// Response
{"status": "ok"}</code></pre>
</div>
<h3>move_window</h3>
<p>Move a window to specified position.</p>
<div class="code-block">
<pre><code>// Request
{
"command": "move_window",
"window": 12345678,
"x": 100,
"y": 100
}
// Response
{"status": "ok"}</code></pre>
</div>
<h3>resize_window</h3>
<p>Resize a window to specified dimensions.</p>
<div class="code-block">
<pre><code>// Request
{
"command": "resize_window",
"window": 12345678,
"width": 800,
"height": 600
}
// Response
{"status": "ok"}</code></pre>
</div>
<h3>minimize_window</h3>
<p>Minimize a window.</p>
<div class="code-block">
<pre><code>{"command": "minimize_window", "window": 12345678}</code></pre>
</div>
<h3>maximize_window</h3>
<p>Maximize a window.</p>
<div class="code-block">
<pre><code>{"command": "maximize_window", "window": 12345678}</code></pre>
</div>
<h3>restore_window</h3>
<p>Restore a minimized or maximized window.</p>
<div class="code-block">
<pre><code>{"command": "restore_window", "window": 12345678}</code></pre>
</div>
<h3>set_fullscreen</h3>
<p>Set window fullscreen state.</p>
<div class="code-block">
<pre><code>{"command": "set_fullscreen", "window": 12345678, "fullscreen": true}</code></pre>
</div>
<h3>set_floating</h3>
<p>Set window floating state.</p>
<div class="code-block">
<pre><code>{"command": "set_floating", "window": 12345678, "floating": true}</code></pre>
</div>
<h3>raise_window</h3>
<p>Raise window to top of stack.</p>
<div class="code-block">
<pre><code>{"command": "raise_window", "window": 12345678}</code></pre>
</div>
<h3>lower_window</h3>
<p>Lower window to bottom of stack.</p>
<div class="code-block">
<pre><code>{"command": "lower_window", "window": 12345678}</code></pre>
</div>
<h2 id="workspaces">Workspace Commands</h2>
<h3>list_workspaces</h3>
<p>Get list of all workspaces.</p>
<div class="code-block">
<pre><code>// Request
{"command": "list_workspaces"}
// Response
{
"status": "ok",
"workspaces": [
{
"index": 0,
"name": "1",
"layout": "tiling",
"window_count": 3,
"current": true
}
],
"current": 0
}</code></pre>
</div>
<h3>switch_workspace</h3>
<p>Switch to specified workspace (0-8).</p>
<div class="code-block">
<pre><code>{"command": "switch_workspace", "workspace": 2}</code></pre>
</div>
<h3>move_to_workspace</h3>
<p>Move focused window to specified workspace.</p>
<div class="code-block">
<pre><code>{"command": "move_to_workspace", "workspace": 3}</code></pre>
</div>
<h3>move_window_to_workspace</h3>
<p>Move specific window to workspace.</p>
<div class="code-block">
<pre><code>{
"command": "move_window_to_workspace",
"window": 12345678,
"workspace": 3
}</code></pre>
</div>
<h2 id="layout">Layout Commands</h2>
<h3>get_layout</h3>
<p>Get current workspace layout.</p>
<div class="code-block">
<pre><code>// Request
{"command": "get_layout"}
// Response
{
"status": "ok",
"layout": "tiling",
"master_ratio": 0.55,
"master_count": 1
}</code></pre>
</div>
<h3>set_layout</h3>
<p>Set workspace layout mode.</p>
<div class="code-block">
<pre><code>// Options: "tiling", "floating", "monocle"
{"command": "set_layout", "layout": "floating"}</code></pre>
</div>
<h3>cycle_layout</h3>
<p>Cycle to next layout mode.</p>
<div class="code-block">
<pre><code>{"command": "cycle_layout"}</code></pre>
</div>
<h3>set_master_ratio</h3>
<p>Set master area ratio (0.1 to 0.9).</p>
<div class="code-block">
<pre><code>{"command": "set_master_ratio", "ratio": 0.6}</code></pre>
</div>
<h3>adjust_master_ratio</h3>
<p>Adjust master ratio by delta.</p>
<div class="code-block">
<pre><code>{"command": "adjust_master_ratio", "delta": 0.05}</code></pre>
</div>
<h3>set_master_count</h3>
<p>Set number of master windows (1 to 10).</p>
<div class="code-block">
<pre><code>{"command": "set_master_count", "count": 2}</code></pre>
</div>
<h3>adjust_master_count</h3>
<p>Adjust master count by delta.</p>
<div class="code-block">
<pre><code>{"command": "adjust_master_count", "delta": 1}</code></pre>
</div>
<h2 id="keyboard">Keyboard Commands</h2>
<h3>key_press</h3>
<p>Simulate key press. Supports modifiers.</p>
<div class="code-block">
<pre><code>// Simple key
{"command": "key_press", "key": "a"}
// With modifiers
{"command": "key_press", "key": "c", "modifiers": ["ctrl"]}
// Special keys
{"command": "key_press", "key": "Return"}
{"command": "key_press", "key": "Tab"}
{"command": "key_press", "key": "Escape"}
{"command": "key_press", "key": "F1"}</code></pre>
</div>
<p>Available modifiers: <code>ctrl</code>, <code>shift</code>, <code>alt</code>, <code>super</code></p>
<h3>key_release</h3>
<p>Simulate key release.</p>
<div class="code-block">
<pre><code>{"command": "key_release", "key": "a"}</code></pre>
</div>
<h3>type_text</h3>
<p>Type a string of text.</p>
<div class="code-block">
<pre><code>{"command": "type_text", "text": "Hello, World!"}</code></pre>
</div>
<h3>key_combo</h3>
<p>Press a key combination.</p>
<div class="code-block">
<pre><code>// Ctrl+Alt+T
{
"command": "key_combo",
"keys": ["ctrl", "alt", "t"]
}
// Ctrl+Shift+S
{
"command": "key_combo",
"keys": ["ctrl", "shift", "s"]
}</code></pre>
</div>
<h2 id="mouse">Mouse Commands</h2>
<h3>mouse_move</h3>
<p>Move mouse to absolute position.</p>
<div class="code-block">
<pre><code>{"command": "mouse_move", "x": 500, "y": 300}</code></pre>
</div>
<h3>mouse_move_relative</h3>
<p>Move mouse relative to current position.</p>
<div class="code-block">
<pre><code>{"command": "mouse_move_relative", "dx": 10, "dy": -5}</code></pre>
</div>
<h3>mouse_click</h3>
<p>Click mouse button.</p>
<div class="code-block">
<pre><code>// Left click at current position
{"command": "mouse_click", "button": 1}
// Right click at position
{"command": "mouse_click", "button": 3, "x": 100, "y": 200}
// Double click
{"command": "mouse_click", "button": 1, "count": 2}</code></pre>
</div>
<p>Buttons: 1=left, 2=middle, 3=right, 4=scroll up, 5=scroll down</p>
<h3>mouse_down</h3>
<p>Press mouse button without releasing.</p>
<div class="code-block">
<pre><code>{"command": "mouse_down", "button": 1}</code></pre>
</div>
<h3>mouse_up</h3>
<p>Release mouse button.</p>
<div class="code-block">
<pre><code>{"command": "mouse_up", "button": 1}</code></pre>
</div>
<h3>mouse_drag</h3>
<p>Drag from one position to another.</p>
<div class="code-block">
<pre><code>{
"command": "mouse_drag",
"from_x": 100,
"from_y": 100,
"to_x": 500,
"to_y": 300,
"button": 1
}</code></pre>
</div>
<h3>mouse_scroll</h3>
<p>Scroll mouse wheel.</p>
<div class="code-block">
<pre><code>// Scroll up
{"command": "mouse_scroll", "direction": "up", "amount": 3}
// Scroll down
{"command": "mouse_scroll", "direction": "down", "amount": 3}</code></pre>
</div>
<h2 id="screenshot">Screenshot Commands</h2>
<h3>screenshot</h3>
<p>Capture screenshot. Returns base64-encoded PNG.</p>
<h4>Async Mode (Default)</h4>
<p>By default, screenshot operations run asynchronously to avoid blocking the API. Set <code>"async": false</code> for synchronous behavior.</p>
<div class="code-block">
<pre><code>// Async (default) - returns when capture completes
{"command": "screenshot", "mode": "fullscreen"}
// Synchronous - blocks until complete
{"command": "screenshot", "mode": "fullscreen", "async": false}</code></pre>
</div>
<h4>Fullscreen Mode</h4>
<div class="code-block">
<pre><code>// Request
{"command": "screenshot", "mode": "fullscreen"}
// Response
{
"status": "ok",
"format": "png",
"encoding": "base64",
"width": 1920,
"height": 1080,
"data": "iVBORw0KGgoAAAANSUhEUg..."
}</code></pre>
</div>
<h4>Active Window Mode</h4>
<div class="code-block">
<pre><code>{"command": "screenshot", "mode": "active"}</code></pre>
</div>
<h4>Specific Window Mode</h4>
<div class="code-block">
<pre><code>{"command": "screenshot", "mode": "window", "window": 12345678}</code></pre>
</div>
<h4>Area Mode</h4>
<div class="code-block">
<pre><code>{
"command": "screenshot",
"mode": "area",
"x": 100,
"y": 100,
"width": 400,
"height": 300
}</code></pre>
</div>
<h2 id="ocr">OCR Commands</h2>
<h3>ocr</h3>
<p>Extract text from image using OCR.</p>
<h4>Async Mode (Default)</h4>
<p>OCR operations run asynchronously by default since text extraction can take 1-10 seconds. Set <code>"async": false</code> for synchronous behavior.</p>
<div class="code-block">
<pre><code>// Async (default)
{"command": "ocr", "image": "...", "language": "eng"}
// Synchronous
{"command": "ocr", "image": "...", "language": "eng", "async": false}</code></pre>
</div>
<h4>Request/Response</h4>
<div class="code-block">
<pre><code>// Request
{
"command": "ocr",
"image": "iVBORw0KGgoAAAANSUhEUg...", // base64 PNG
"language": "eng" // optional, default "eng"
}
// Response
{
"status": "ok",
"text": "Extracted text from image...",
"confidence": 0.95
}</code></pre>
</div>
<p>Common language codes: <code>eng</code> (English), <code>deu</code> (German), <code>fra</code> (French), <code>spa</code> (Spanish)</p>
<h2 id="events">Event Subscription</h2>
<p>DWN supports real-time event streaming over WebSocket. Subscribe to events to receive notifications about window actions, workspace changes, input events, and more.</p>
<h3>list_events</h3>
<p>Get list of all available event types.</p>
<div class="code-block">
<pre><code>// Request
{"command": "list_events"}
// Response
{
"status": "ok",
"events": [
"window_created",
"window_destroyed",
"window_focused",
"window_unfocused",
"window_moved",
"window_resized",
"window_minimized",
"window_restored",
"window_maximized",
"window_unmaximized",
"window_fullscreen",
"window_unfullscreen",
"window_floating",
"window_unfloating",
"window_title_changed",
"window_raised",
"window_lowered",
"workspace_switched",
"workspace_window_added",
"workspace_window_removed",
"workspace_layout_changed",
"workspace_master_ratio_changed",
"workspace_master_count_changed",
"workspace_arranged",
"layout_changed",
"key_pressed",
"key_released",
"shortcut_triggered",
"mouse_moved",
"mouse_button_pressed",
"mouse_button_released",
"drag_started",
"drag_ended",
"show_desktop_toggled",
"notification_shown",
"notification_closed"
]
}</code></pre>
</div>
<h3>subscribe</h3>
<p>Subscribe to specific events or all events.</p>
<div class="code-block">
<pre><code>// Subscribe to specific events
{"command": "subscribe", "events": ["window_focused", "window_created"]}
// Subscribe to all events
{"command": "subscribe", "all": true}
// Response
{
"status": "ok",
"subscribed": ["window_focused", "window_created"]
}</code></pre>
</div>
<h3>unsubscribe</h3>
<p>Unsubscribe from specific events or all events.</p>
<div class="code-block">
<pre><code>// Unsubscribe from specific events
{"command": "unsubscribe", "events": ["window_focused"]}
// Unsubscribe from all events
{"command": "unsubscribe", "all": true}
// Response
{"status": "ok"}</code></pre>
</div>
<h3>Event Message Format</h3>
<p>When subscribed, events are pushed to the client as JSON messages:</p>
<div class="code-block">
<pre><code>// Window focus event
{
"type": "event",
"event": "window_focused",
"data": {
"window": 12345678,
"title": "Firefox",
"previous_window": 87654321
}
}
// Window moved event
{
"type": "event",
"event": "window_moved",
"data": {
"window": 12345678,
"old_x": 0,
"old_y": 32,
"new_x": 100,
"new_y": 100
}
}
// Workspace switched event
{
"type": "event",
"event": "workspace_switched",
"data": {
"old_workspace": 0,
"new_workspace": 2
}
}
// Keyboard shortcut triggered
{
"type": "event",
"event": "shortcut_triggered",
"data": {
"name": "Return",
"description": "Open terminal"
}
}
// Notification shown
{
"type": "event",
"event": "notification_shown",
"data": {
"id": 1,
"summary": "New Message",
"body": "You have a new message"
}
}</code></pre>
</div>
<h3>Event Types Reference</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
<th>Data Fields</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>window_created</code></td>
<td>New window managed</td>
<td>window, title, class, workspace</td>
</tr>
<tr>
<td><code>window_destroyed</code></td>
<td>Window closed</td>
<td>window, title, workspace</td>
</tr>
<tr>
<td><code>window_focused</code></td>
<td>Window received focus</td>
<td>window, title, previous_window</td>
</tr>
<tr>
<td><code>window_unfocused</code></td>
<td>Window lost focus</td>
<td>window, title</td>
</tr>
<tr>
<td><code>window_moved</code></td>
<td>Window position changed</td>
<td>window, old_x, old_y, new_x, new_y</td>
</tr>
<tr>
<td><code>window_resized</code></td>
<td>Window size changed</td>
<td>window, old_width, old_height, new_width, new_height</td>
</tr>
<tr>
<td><code>window_minimized</code></td>
<td>Window minimized</td>
<td>window</td>
</tr>
<tr>
<td><code>window_restored</code></td>
<td>Window restored from minimized</td>
<td>window</td>
</tr>
<tr>
<td><code>window_maximized</code></td>
<td>Window maximized/unmaximized</td>
<td>window, maximized</td>
</tr>
<tr>
<td><code>window_fullscreen</code></td>
<td>Fullscreen toggled</td>
<td>window, fullscreen</td>
</tr>
<tr>
<td><code>window_floating</code></td>
<td>Floating mode toggled</td>
<td>window, floating</td>
</tr>
<tr>
<td><code>window_title_changed</code></td>
<td>Window title updated</td>
<td>window, old_title, new_title</td>
</tr>
<tr>
<td><code>workspace_switched</code></td>
<td>Active workspace changed</td>
<td>old_workspace, new_workspace</td>
</tr>
<tr>
<td><code>workspace_layout_changed</code></td>
<td>Workspace layout changed</td>
<td>workspace, old_layout, new_layout</td>
</tr>
<tr>
<td><code>key_pressed</code></td>
<td>Key pressed</td>
<td>keycode, keysym, modifiers</td>
</tr>
<tr>
<td><code>shortcut_triggered</code></td>
<td>Keyboard shortcut executed</td>
<td>name, description</td>
</tr>
<tr>
<td><code>mouse_button_pressed</code></td>
<td>Mouse button pressed</td>
<td>button, x, y</td>
</tr>
<tr>
<td><code>drag_started</code></td>
<td>Window drag/resize started</td>
<td>window, is_resize</td>
</tr>
<tr>
<td><code>drag_ended</code></td>
<td>Window drag/resize ended</td>
<td>window, is_resize</td>
</tr>
<tr>
<td><code>show_desktop_toggled</code></td>
<td>Show desktop mode toggled</td>
<td>shown</td>
</tr>
<tr>
<td><code>notification_shown</code></td>
<td>Notification displayed</td>
<td>id, summary, body</td>
</tr>
<tr>
<td><code>notification_closed</code></td>
<td>Notification closed</td>
<td>id</td>
</tr>
</tbody>
</table>
</div>
<h2 id="system">System Commands</h2>
<h3>get_monitors</h3>
<p>Get monitor information.</p>
<div class="code-block">
<pre><code>// Response
{
"status": "ok",
"monitors": [
{
"index": 0,
"x": 0,
"y": 0,
"width": 1920,
"height": 1080,
"primary": true
}
]
}</code></pre>
</div>
<h3>spawn</h3>
<p>Spawn a process.</p>
<div class="code-block">
<pre><code>{"command": "spawn", "program": "firefox"}</code></pre>
</div>
<h3>notify</h3>
<p>Show a notification.</p>
<div class="code-block">
<pre><code>{
"command": "notify",
"summary": "Title",
"body": "Notification body text",
"timeout": 5000
}</code></pre>
</div>
<h3>get_focused</h3>
<p>Get currently focused window.</p>
<div class="code-block">
<pre><code>// Response
{
"status": "ok",
"window": {
"id": 12345678,
"title": "...",
...
}
}</code></pre>
</div>
<h3>show_desktop</h3>
<p>Toggle show desktop mode.</p>
<div class="code-block">
<pre><code>{"command": "show_desktop"}</code></pre>
</div>
<h3>quit</h3>
<p>Quit DWN.</p>
<div class="code-block">
<pre><code>{"command": "quit"}</code></pre>
</div>
<h2>Error Responses</h2>
<p>All commands may return errors:</p>
<div class="code-block">
<pre><code>{
"status": "error",
"message": "Window not found"
}
{
"status": "error",
"message": "Invalid parameter: workspace must be 0-8"
}
{
"status": "error",
"message": "Unknown command: invalid_command"
}</code></pre>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

510
manual/architecture.html Normal file
View File

@ -0,0 +1,510 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Architecture - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link active">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Architecture</h1>
<p class="lead">Technical overview of DWN's design and implementation</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#overview">Overview</a></li>
<li><a href="#modules">Module Structure</a></li>
<li><a href="#event-loop">Event Loop</a></li>
<li><a href="#data-structures">Core Data Structures</a></li>
<li><a href="#protocols">X11 Protocols</a></li>
<li><a href="#design-patterns">Design Patterns</a></li>
</ul>
</div>
<h2 id="overview">Overview</h2>
<p>DWN is written in ANSI C for X11/Xorg. It uses a modular architecture with a global state singleton and event-driven design.</p>
<div class="code-block">
<pre><code>┌─────────────────────────────────────────────────────────┐
│ DWN Window Manager │
├─────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ main │ │ keys │ │ panel │ │ api │ │
│ │ loop │ │ handler │ │ render │ │ server │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ ┌────┴────────────┴────────────┴────────────┴────┐ │
│ │ DWNState (Global Singleton) │ │
│ └────┬────────────┬────────────┬────────────┬────┘ │
│ │ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ client │ │workspace│ │ layout │ │ atoms │ │
│ │ mgmt │ │ mgmt │ │ engine │ │ (EWMH) │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
├─────────────────────────────────────────────────────────┤
│ X11 / Xlib │
└─────────────────────────────────────────────────────────┘</code></pre>
</div>
<h2 id="modules">Module Structure</h2>
<p>Each module has a header in <code>include/</code> and implementation in <code>src/</code>.</p>
<div class="table-container">
<table>
<thead>
<tr>
<th>Module</th>
<th>Responsibility</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>main.c</code></td>
<td>X11 initialization, event loop, signal handling</td>
</tr>
<tr>
<td><code>client.c</code></td>
<td>Window management, focus, frame creation</td>
</tr>
<tr>
<td><code>workspace.c</code></td>
<td>9 virtual desktops, per-workspace state</td>
</tr>
<tr>
<td><code>layout.c</code></td>
<td>Tiling, floating, monocle algorithms</td>
</tr>
<tr>
<td><code>decorations.c</code></td>
<td>Window title bars and borders</td>
</tr>
<tr>
<td><code>panel.c</code></td>
<td>Top/bottom panels, taskbar, workspace indicators</td>
</tr>
<tr>
<td><code>systray.c</code></td>
<td>XEmbed system tray, WiFi/audio/battery widgets</td>
</tr>
<tr>
<td><code>notifications.c</code></td>
<td>D-Bus notification daemon</td>
</tr>
<tr>
<td><code>atoms.c</code></td>
<td>X11 EWMH/ICCCM atom management</td>
</tr>
<tr>
<td><code>keys.c</code></td>
<td>Keyboard shortcut capture and callbacks</td>
</tr>
<tr>
<td><code>config.c</code></td>
<td>INI-style config loading</td>
</tr>
<tr>
<td><code>api.c</code></td>
<td>WebSocket JSON API server</td>
</tr>
<tr>
<td><code>screenshot.c</code></td>
<td>X11 capture + PNG encoding</td>
</tr>
<tr>
<td><code>ocr.c</code></td>
<td>Tesseract OCR integration</td>
</tr>
<tr>
<td><code>ai.c</code></td>
<td>OpenRouter API, Exa search</td>
</tr>
<tr>
<td><code>util.c</code></td>
<td>Logging, memory, string utilities</td>
</tr>
</tbody>
</table>
</div>
<h2 id="event-loop">Event Loop</h2>
<p>The main event loop uses <code>select()</code> for multiplexed I/O across X11, D-Bus, and timers.</p>
<div class="code-block">
<pre><code>void dwn_run(void) {
int x11_fd = ConnectionNumber(dwn->display);
int dbus_fd = /* from dbus_connection */;
while (dwn->running) {
// 1. Process all pending X11 events
while (XPending(dwn->display)) {
XEvent ev;
XNextEvent(dwn->display, &ev);
dwn_handle_event(&ev);
}
// 2. Process D-Bus messages
notifications_process_messages();
// 3. Process async AI/Exa requests
ai_process_pending();
// 4. Check notification timeouts
notifications_update();
// 5. Handle delayed focus (focus-follow mode)
handle_pending_focus();
// 6. Periodic updates (animation, clock)
if (now - last_update >= 16) { // 60fps
news_update();
panel_render_all();
}
// 7. Wait for events with timeout
fd_set fds;
FD_SET(x11_fd, &fds);
FD_SET(dbus_fd, &fds);
struct timeval tv = {0, 16000}; // 16ms
select(max_fd + 1, &fds, NULL, NULL, &tv);
}
}</code></pre>
</div>
<h3>X11 Event Dispatch</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Event</th>
<th>Handler</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MapRequest</code></td>
<td>New window → client_manage()</td>
</tr>
<tr>
<td><code>UnmapNotify</code></td>
<td>Window hidden → possibly client_unmanage()</td>
</tr>
<tr>
<td><code>DestroyNotify</code></td>
<td>Window destroyed → client_unmanage()</td>
</tr>
<tr>
<td><code>ConfigureRequest</code></td>
<td>Window resize request</td>
</tr>
<tr>
<td><code>PropertyNotify</code></td>
<td>Property changed → update title</td>
</tr>
<tr>
<td><code>Expose</code></td>
<td>Repaint needed → render</td>
</tr>
<tr>
<td><code>ButtonPress</code></td>
<td>Mouse click → focus, drag, panel click</td>
</tr>
<tr>
<td><code>MotionNotify</code></td>
<td>Mouse move → window drag/resize</td>
</tr>
<tr>
<td><code>KeyPress</code></td>
<td>Key pressed → shortcut callback</td>
</tr>
<tr>
<td><code>ClientMessage</code></td>
<td>EWMH requests, systray dock</td>
</tr>
</tbody>
</table>
</div>
<h2 id="data-structures">Core Data Structures</h2>
<h3>DWNState</h3>
<p>Global singleton containing all window manager state.</p>
<div class="code-block">
<pre><code>typedef struct {
Display *display; // X11 connection
int screen; // Default screen
Window root; // Root window
int screen_width, screen_height;
Monitor monitors[MAX_MONITORS];
int monitor_count;
Workspace workspaces[MAX_WORKSPACES]; // 9 workspaces
int current_workspace;
Client *client_list; // Doubly-linked list head
int client_count;
Panel *top_panel;
Panel *bottom_panel;
Config *config;
bool running;
bool ai_enabled;
// Drag state
Client *drag_client;
int drag_start_x, drag_start_y;
bool resizing;
// Alt-Tab state
bool is_alt_tabbing;
Client *alt_tab_client;
} DWNState;</code></pre>
</div>
<h3>Client</h3>
<p>Represents a managed window with decoration frame.</p>
<div class="code-block">
<pre><code>struct Client {
Window window; // Application window
Window frame; // Decoration frame (parent)
int x, y, width, height; // Current geometry
int old_x, old_y; // Saved for restore
int old_width, old_height;
uint32_t flags; // CLIENT_FLOATING, etc.
unsigned int workspace; // Workspace index (0-8)
char title[256];
char class[64];
SnapConstraint snap; // Snap state
Client *next, *prev; // Global list
Client *mru_next, *mru_prev; // MRU stack
};</code></pre>
</div>
<h3>Workspace</h3>
<p>Per-workspace layout and window state.</p>
<div class="code-block">
<pre><code>struct Workspace {
Client *clients; // Workspace client list
Client *focused; // Currently focused
Client *mru_head, *mru_tail; // MRU stack
LayoutType layout; // tiling/floating/monocle
float master_ratio; // 0.1 to 0.9
int master_count; // 1 to 10
char name[32];
};</code></pre>
</div>
<h3>Client Flags</h3>
<div class="code-block">
<pre><code>#define CLIENT_NORMAL 0
#define CLIENT_FLOATING (1 << 0)
#define CLIENT_FULLSCREEN (1 << 1)
#define CLIENT_URGENT (1 << 2)
#define CLIENT_MINIMIZED (1 << 3)
#define CLIENT_STICKY (1 << 4)
#define CLIENT_MAXIMIZED (1 << 5)
// Usage
if (c->flags & CLIENT_FLOATING) { ... }
c->flags |= CLIENT_FLOATING; // Set
c->flags &= ~CLIENT_FLOATING; // Clear</code></pre>
</div>
<h2 id="protocols">X11 Protocols</h2>
<h3>EWMH (Extended Window Manager Hints)</h3>
<p>Standard hints for modern window manager features.</p>
<ul>
<li><code>_NET_SUPPORTED</code> - List of supported atoms</li>
<li><code>_NET_CLIENT_LIST</code> - List of managed windows</li>
<li><code>_NET_CURRENT_DESKTOP</code> - Current workspace</li>
<li><code>_NET_ACTIVE_WINDOW</code> - Focused window</li>
<li><code>_NET_WM_STATE</code> - Window state (fullscreen, etc.)</li>
<li><code>_NET_WM_WINDOW_TYPE</code> - Window type (dialog, etc.)</li>
</ul>
<h3>ICCCM (Inter-Client Communication)</h3>
<p>Core X11 window manager protocol.</p>
<ul>
<li><code>WM_PROTOCOLS</code> - Supported protocols (WM_DELETE_WINDOW)</li>
<li><code>WM_NAME</code> - Window title</li>
<li><code>WM_CLASS</code> - Application class</li>
<li><code>WM_HINTS</code> - Input model, icons</li>
<li><code>WM_NORMAL_HINTS</code> - Size constraints</li>
</ul>
<h3>XEmbed (System Tray)</h3>
<p>Protocol for embedding application icons in system tray.</p>
<ul>
<li>Acquire <code>_NET_SYSTEM_TRAY_S0</code> selection</li>
<li>Handle <code>SYSTEM_TRAY_REQUEST_DOCK</code> messages</li>
<li>Send <code>XEMBED_EMBEDDED_NOTIFY</code> to docked icons</li>
</ul>
<h2 id="design-patterns">Design Patterns</h2>
<h3>Opaque Pointers</h3>
<p>Hide implementation details. Header exposes typedef, source defines struct.</p>
<div class="code-block">
<pre><code>// header.h
typedef struct module_t* Module;
Module module_create(void);
void module_destroy(Module m);
// source.c
struct module_t {
int private_field;
};</code></pre>
</div>
<h3>Return Code Error Handling</h3>
<p>Functions return status codes, pass results via output parameters.</p>
<div class="code-block">
<pre><code>typedef enum {
STATUS_OK = 0,
STATUS_ERROR_INVALID_ARG,
STATUS_ERROR_NO_MEMORY
} Status;
Status do_work(int input, int *output);</code></pre>
</div>
<h3>Goto Cleanup</h3>
<p>Centralized resource cleanup for functions with multiple allocations.</p>
<div class="code-block">
<pre><code>int process(void) {
char *buf = NULL;
int status = -1;
buf = malloc(1024);
if (!buf) goto cleanup;
// ... work ...
status = 0;
cleanup:
free(buf);
return status;
}</code></pre>
</div>
<h3>Module Prefix Convention</h3>
<p>All public functions prefixed with module name.</p>
<div class="code-block">
<pre><code>// client.h
void client_focus(Client *c);
void client_move(Client *c, int x, int y);
void client_resize(Client *c, int w, int h);
// workspace.h
void workspace_switch(int index);
void workspace_arrange(int index);</code></pre>
</div>
<h2>Key Constants</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MAX_CLIENTS</code></td>
<td>256</td>
<td>Maximum managed windows</td>
</tr>
<tr>
<td><code>MAX_WORKSPACES</code></td>
<td>9</td>
<td>Virtual desktops</td>
</tr>
<tr>
<td><code>MAX_MONITORS</code></td>
<td>8</td>
<td>Multi-monitor support</td>
</tr>
<tr>
<td><code>MAX_NOTIFICATIONS</code></td>
<td>32</td>
<td>Visible notifications</td>
</tr>
<tr>
<td><code>MAX_KEYBINDINGS</code></td>
<td>64</td>
<td>Keyboard shortcuts</td>
</tr>
<tr>
<td><code>MAX_TRAY_ICONS</code></td>
<td>32</td>
<td>System tray icons</td>
</tr>
</tbody>
</table>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

438
manual/building.html Normal file
View File

@ -0,0 +1,438 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Building from Source - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link active">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Building from Source</h1>
<p class="lead">Compile DWN for development or custom builds</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#requirements">Requirements</a></li>
<li><a href="#dependencies">Dependencies</a></li>
<li><a href="#building">Building</a></li>
<li><a href="#targets">Make Targets</a></li>
<li><a href="#development">Development</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
</ul>
</div>
<h2 id="requirements">Requirements</h2>
<ul>
<li>Linux with X11 (Xorg)</li>
<li>GCC compiler (or Clang)</li>
<li>GNU Make</li>
<li>pkg-config</li>
</ul>
<h2 id="dependencies">Dependencies</h2>
<h3>Required Libraries</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Library</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>libX11</td>
<td>Core X11 protocol</td>
</tr>
<tr>
<td>libXext</td>
<td>X11 extensions</td>
</tr>
<tr>
<td>libXinerama</td>
<td>Multi-monitor support</td>
</tr>
<tr>
<td>libXrandr</td>
<td>Display configuration</td>
</tr>
<tr>
<td>libXft</td>
<td>Font rendering</td>
</tr>
<tr>
<td>libXtst</td>
<td>Input simulation (API)</td>
</tr>
<tr>
<td>fontconfig</td>
<td>Font discovery</td>
</tr>
<tr>
<td>libdbus-1</td>
<td>Notifications</td>
</tr>
<tr>
<td>libcurl</td>
<td>HTTP for AI features</td>
</tr>
<tr>
<td>libpng</td>
<td>Screenshot encoding</td>
</tr>
<tr>
<td>libtesseract</td>
<td>OCR text extraction</td>
</tr>
<tr>
<td>libleptonica</td>
<td>Image processing for OCR</td>
</tr>
</tbody>
</table>
</div>
<h3>Ubuntu / Debian</h3>
<div class="code-block">
<pre><code>sudo apt update && sudo apt install -y \
build-essential \
pkg-config \
libx11-dev \
libxext-dev \
libxinerama-dev \
libxrandr-dev \
libxft-dev \
libxtst-dev \
libfontconfig1-dev \
libdbus-1-dev \
libcurl4-openssl-dev \
libpng-dev \
libtesseract-dev \
libleptonica-dev \
tesseract-ocr \
tesseract-ocr-eng</code></pre>
</div>
<h3>Fedora / RHEL</h3>
<div class="code-block">
<pre><code>sudo dnf install -y \
gcc make \
pkg-config \
libX11-devel \
libXext-devel \
libXinerama-devel \
libXrandr-devel \
libXtst-devel \
dbus-devel \
libcurl-devel \
libpng-devel \
tesseract-devel \
leptonica-devel \
tesseract-langpack-eng</code></pre>
</div>
<h3>Arch Linux</h3>
<div class="code-block">
<pre><code>sudo pacman -S --needed \
base-devel \
pkg-config \
libx11 \
libxext \
libxinerama \
libxrandr \
libxtst \
dbus \
curl \
libpng \
tesseract \
tesseract-data-eng \
leptonica</code></pre>
</div>
<h3>Automatic Installation</h3>
<p>The Makefile can auto-detect your package manager:</p>
<div class="code-block">
<pre><code>make deps</code></pre>
</div>
<h2 id="building">Building</h2>
<h3>Clone Repository</h3>
<div class="code-block">
<pre><code>git clone https://github.com/retoor/dwn.git
cd dwn</code></pre>
</div>
<h3>Build Release</h3>
<div class="code-block">
<pre><code>make</code></pre>
</div>
<p>Output: <code>bin/dwn</code></p>
<h3>Build Debug</h3>
<div class="code-block">
<pre><code>make debug</code></pre>
</div>
<p>Includes debug symbols (<code>-g</code>) and <code>-DDEBUG</code> define.</p>
<h3>Build with Sanitizers</h3>
<div class="code-block">
<pre><code>make sanitize</code></pre>
</div>
<p>Enables AddressSanitizer and UndefinedBehaviorSanitizer for debugging memory issues.</p>
<h3>Install System-wide</h3>
<div class="code-block">
<pre><code>sudo make install</code></pre>
</div>
<p>Installs to:</p>
<ul>
<li><code>/usr/local/bin/dwn</code> - Binary</li>
<li><code>/usr/local/share/xsessions/dwn.desktop</code> - Session file</li>
<li><code>/etc/dwn/config.example</code> - Example config</li>
</ul>
<h3>Custom Prefix</h3>
<div class="code-block">
<pre><code>make PREFIX=/opt/dwn install</code></pre>
</div>
<h2 id="targets">Make Targets</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Target</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>make</code></td>
<td>Build release version (optimized)</td>
</tr>
<tr>
<td><code>make debug</code></td>
<td>Build with debug symbols</td>
</tr>
<tr>
<td><code>make sanitize</code></td>
<td>Build with sanitizers</td>
</tr>
<tr>
<td><code>make clean</code></td>
<td>Remove build artifacts</td>
</tr>
<tr>
<td><code>make install</code></td>
<td>Install to PREFIX</td>
</tr>
<tr>
<td><code>make uninstall</code></td>
<td>Remove installation</td>
</tr>
<tr>
<td><code>make run</code></td>
<td>Test in Xephyr</td>
</tr>
<tr>
<td><code>make deps</code></td>
<td>Install dependencies</td>
</tr>
<tr>
<td><code>make format</code></td>
<td>Run clang-format</td>
</tr>
<tr>
<td><code>make check</code></td>
<td>Run cppcheck static analysis</td>
</tr>
</tbody>
</table>
</div>
<h2 id="development">Development</h2>
<h3>Testing in Xephyr</h3>
<p>Test without affecting your current session:</p>
<div class="code-block">
<pre><code># Automatic (recommended)
make run
# Manual
Xephyr :1 -screen 1920x1080 &amp;
DISPLAY=:1 ./bin/dwn</code></pre>
</div>
<h3>Code Formatting</h3>
<div class="code-block">
<pre><code>make format</code></pre>
</div>
<p>Uses clang-format with project style.</p>
<h3>Static Analysis</h3>
<div class="code-block">
<pre><code>make check</code></pre>
</div>
<p>Runs cppcheck for common issues.</p>
<h3>Debug Logging</h3>
<p>Debug builds write to <code>~/.local/share/dwn/dwn.log</code>:</p>
<div class="code-block">
<pre><code>tail -f ~/.local/share/dwn/dwn.log</code></pre>
</div>
<h3>Directory Structure</h3>
<div class="code-block">
<pre><code>dwn/
├── include/ # Header files
│ ├── dwn.h # Main state struct
│ ├── client.h # Window management
│ ├── workspace.h # Virtual desktops
│ ├── layout.h # Layout algorithms
│ ├── panel.h # Panels
│ ├── api.h # WebSocket API
│ ├── screenshot.h
│ ├── ocr.h
│ └── ...
├── src/ # Implementation
│ ├── main.c # Entry point, event loop
│ ├── client.c
│ ├── workspace.c
│ ├── layout.c
│ ├── panel.c
│ ├── api.c
│ ├── screenshot.c
│ ├── ocr.c
│ └── ...
├── build/ # Object files
├── bin/ # Output binary
├── examples/ # Client examples
├── manual/ # Documentation
├── Makefile
└── CLAUDE.md # Development guide</code></pre>
</div>
<h3>Adding a New Module</h3>
<ol>
<li>Create <code>include/newmodule.h</code> with public API</li>
<li>Create <code>src/newmodule.c</code> with implementation</li>
<li>Add <code>newmodule_init()</code> call to <code>main.c:dwn_init()</code></li>
<li>Add <code>newmodule_cleanup()</code> call to <code>main.c:dwn_cleanup()</code></li>
<li>Makefile automatically picks up new .c files</li>
</ol>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3>Missing pkg-config</h3>
<div class="code-block">
<pre><code># Check if library is found
pkg-config --cflags --libs x11
# If not found, install dev package
sudo apt install libx11-dev # Debian/Ubuntu</code></pre>
</div>
<h3>Tesseract not found</h3>
<div class="code-block">
<pre><code># Check installation
pkg-config --cflags --libs tesseract
# Install if missing
sudo apt install libtesseract-dev tesseract-ocr tesseract-ocr-eng</code></pre>
</div>
<h3>Linker errors</h3>
<p>Ensure all -dev packages are installed. The Makefile uses pkg-config to find libraries.</p>
<h3>Runtime errors</h3>
<ol>
<li>Check log file: <code>~/.local/share/dwn/dwn.log</code></li>
<li>Build with debug: <code>make clean && make debug</code></li>
<li>Build with sanitizers: <code>make clean && make sanitize</code></li>
<li>Run in GDB: <code>DISPLAY=:1 gdb ./bin/dwn</code></li>
</ol>
<h3>X11 errors</h3>
<p>Run with synchronous X11 for detailed errors:</p>
<div class="code-block">
<pre><code>DISPLAY=:1 ./bin/dwn --sync</code></pre>
</div>
<h3>Xephyr not starting</h3>
<div class="code-block">
<pre><code># Check if display :1 is in use
ls /tmp/.X11-unix/
# Use different display
Xephyr :2 -screen 1920x1080 &amp;
DISPLAY=:2 ./bin/dwn</code></pre>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

389
manual/configuration.html Normal file
View File

@ -0,0 +1,389 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Configuration</h1>
<p class="lead">Customize DWN to your preferences</p>
</div>
<h2>Configuration File</h2>
<p>DWN uses an INI-style configuration file located at:</p>
<div class="code-block">
<pre><code>~/.config/dwn/config</code></pre>
</div>
<p>Create it from the example:</p>
<div class="code-block">
<pre><code>mkdir -p ~/.config/dwn
cp /etc/dwn/config.example ~/.config/dwn/config</code></pre>
</div>
<h2>Configuration Sections</h2>
<h3>[general] - Core Settings</h3>
<div class="code-block">
<pre><code>[general]
terminal = xfce4-terminal
launcher = dmenu_run
file_manager = thunar
focus_mode = click
focus_follow_delay = 100
decorations = true</code></pre>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>terminal</code></td>
<td>xfce4-terminal</td>
<td>Terminal emulator command</td>
</tr>
<tr>
<td><code>launcher</code></td>
<td>dmenu_run</td>
<td>Application launcher command</td>
</tr>
<tr>
<td><code>file_manager</code></td>
<td>thunar</td>
<td>File manager command</td>
</tr>
<tr>
<td><code>focus_mode</code></td>
<td>click</td>
<td><code>click</code> or <code>follow</code> (sloppy focus)</td>
</tr>
<tr>
<td><code>focus_follow_delay</code></td>
<td>100</td>
<td>Delay in ms for focus-follow mode (0-1000)</td>
</tr>
<tr>
<td><code>decorations</code></td>
<td>true</td>
<td>Show window decorations (title bar)</td>
</tr>
</tbody>
</table>
</div>
<h3>[appearance] - Visual Settings</h3>
<div class="code-block">
<pre><code>[appearance]
border_width = 0
title_height = 28
panel_height = 32
gap = 0
font = fixed</code></pre>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Option</th>
<th>Range</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border_width</code></td>
<td>0-50</td>
<td>Window border width in pixels</td>
</tr>
<tr>
<td><code>title_height</code></td>
<td>0-100</td>
<td>Title bar height in pixels</td>
</tr>
<tr>
<td><code>panel_height</code></td>
<td>0-100</td>
<td>Panel height in pixels</td>
</tr>
<tr>
<td><code>gap</code></td>
<td>0-100</td>
<td>Gap between windows in pixels</td>
</tr>
<tr>
<td><code>font</code></td>
<td>-</td>
<td>X11 font name</td>
</tr>
</tbody>
</table>
</div>
<h3>[layout] - Layout Behavior</h3>
<div class="code-block">
<pre><code>[layout]
default = tiling
master_ratio = 0.55
master_count = 1</code></pre>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Option</th>
<th>Values</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>default</code></td>
<td>tiling, floating, monocle</td>
<td>Default layout for new workspaces</td>
</tr>
<tr>
<td><code>master_ratio</code></td>
<td>0.1-0.9</td>
<td>Master area ratio in tiling layout</td>
</tr>
<tr>
<td><code>master_count</code></td>
<td>1-10</td>
<td>Number of windows in master area</td>
</tr>
</tbody>
</table>
</div>
<h3>[panels] - Panel Visibility</h3>
<div class="code-block">
<pre><code>[panels]
top = true
bottom = true</code></pre>
</div>
<h3>[colors] - Color Scheme</h3>
<p>Colors are specified in hex format <code>#RRGGBB</code>.</p>
<div class="code-block">
<pre><code>[colors]
panel_bg = #1a1a2e
panel_fg = #e0e0e0
workspace_active = #4a90d9
workspace_inactive = #3a3a4e
workspace_urgent = #d94a4a
title_focused_bg = #2d3a4a
title_focused_fg = #ffffff
title_unfocused_bg = #1a1a1a
title_unfocused_fg = #808080
border_focused = #4a90d9
border_unfocused = #333333
notification_bg = #2a2a3e
notification_fg = #ffffff</code></pre>
</div>
<h3>[api] - WebSocket API</h3>
<div class="code-block">
<pre><code>[api]
enabled = true
port = 8777</code></pre>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>enabled</code></td>
<td>false</td>
<td>Enable WebSocket API server</td>
</tr>
<tr>
<td><code>port</code></td>
<td>8777</td>
<td>API server port number</td>
</tr>
</tbody>
</table>
</div>
<h3>[ai] - AI Integration</h3>
<div class="code-block">
<pre><code>[ai]
model = google/gemini-2.0-flash-exp:free
openrouter_api_key = sk-or-v1-your-key
exa_api_key = your-exa-key</code></pre>
</div>
<p>API keys can also be set via environment variables:</p>
<ul>
<li><code>OPENROUTER_API_KEY</code></li>
<li><code>EXA_API_KEY</code></li>
</ul>
<h3>[autostart] - XDG Autostart</h3>
<div class="code-block">
<pre><code>[autostart]
enabled = true
xdg_autostart = true
path = ~/.config/dwn/autostart.d</code></pre>
</div>
<p>Directories scanned when <code>xdg_autostart = true</code>:</p>
<ul>
<li><code>/etc/xdg/autostart/*.desktop</code> - System defaults</li>
<li><code>~/.config/autostart/*.desktop</code> - User autostart</li>
<li><code>~/.config/dwn/autostart.d/*</code> - DWN-specific scripts</li>
</ul>
<h3>[demo] - Demo Mode Timing</h3>
<div class="code-block">
<pre><code>[demo]
step_delay = 4000
ai_timeout = 15000
window_timeout = 5000</code></pre>
</div>
<h2>Environment Variables</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>OPENROUTER_API_KEY</code></td>
<td>API key for AI command palette</td>
</tr>
<tr>
<td><code>EXA_API_KEY</code></td>
<td>API key for Exa semantic search</td>
</tr>
<tr>
<td><code>DISPLAY</code></td>
<td>X server to connect to</td>
</tr>
</tbody>
</table>
</div>
<h2>Example Configuration</h2>
<div class="code-block">
<pre><code>[general]
terminal = alacritty
launcher = rofi -show drun
file_manager = nautilus
focus_mode = click
decorations = true
[appearance]
border_width = 2
title_height = 24
panel_height = 28
gap = 5
font = monospace
[layout]
default = tiling
master_ratio = 0.55
master_count = 1
[panels]
top = true
bottom = false
[colors]
panel_bg = #282c34
panel_fg = #abb2bf
workspace_active = #61afef
workspace_inactive = #3e4451
border_focused = #61afef
border_unfocused = #3e4451
[api]
enabled = true
port = 8777
[autostart]
enabled = true
xdg_autostart = true</code></pre>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

611
manual/css/style.css Normal file
View File

@ -0,0 +1,611 @@
/* retoor <retoor@molodetz.nl> */
/* DWN Documentation Styles */
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-tertiary: #0f3460;
--bg-code: #0d1117;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-muted: #666;
--accent: #4a90d9;
--accent-hover: #5da0e9;
--border-color: #2d3748;
--success: #48bb78;
--warning: #ed8936;
--error: #f56565;
--sidebar-width: 280px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
a {
color: var(--accent);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--accent-hover);
}
.layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
position: fixed;
top: 0;
left: 0;
height: 100vh;
overflow-y: auto;
z-index: 100;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header h1 {
font-size: 1.5rem;
color: var(--accent);
margin-bottom: 5px;
}
.sidebar-header .version {
font-size: 0.85rem;
color: var(--text-muted);
}
.sidebar-nav {
padding: 15px 0;
}
.nav-section {
margin-bottom: 10px;
}
.nav-section-title {
padding: 8px 20px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
font-weight: 600;
}
.nav-link {
display: block;
padding: 8px 20px 8px 30px;
color: var(--text-secondary);
font-size: 0.9rem;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.nav-link:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-link.active {
background: var(--bg-tertiary);
color: var(--accent);
border-left-color: var(--accent);
}
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-width: 0;
}
.content {
max-width: 900px;
margin: 0 auto;
padding: 40px;
}
.page-header {
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.page-header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: var(--text-primary);
}
.page-header .lead {
font-size: 1.2rem;
color: var(--text-secondary);
}
h2 {
font-size: 1.75rem;
margin: 40px 0 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
h3 {
font-size: 1.35rem;
margin: 30px 0 15px;
color: var(--text-primary);
}
h4 {
font-size: 1.1rem;
margin: 20px 0 10px;
color: var(--text-secondary);
}
p {
margin-bottom: 15px;
}
ul, ol {
margin: 0 0 20px 25px;
}
li {
margin-bottom: 8px;
}
code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--bg-code);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
color: var(--accent);
}
pre {
background: var(--bg-code);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
overflow-x: auto;
margin: 20px 0;
}
pre code {
background: none;
padding: 0;
color: var(--text-primary);
font-size: 0.85rem;
line-height: 1.5;
}
.table-container {
overflow-x: auto;
margin: 20px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: var(--bg-secondary);
font-weight: 600;
color: var(--text-primary);
}
tr:hover {
background: var(--bg-secondary);
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 25px;
margin: 20px 0;
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-primary);
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin: 30px 0;
}
.feature-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 25px;
transition: transform 0.2s, border-color 0.2s;
}
.feature-card:hover {
transform: translateY(-2px);
border-color: var(--accent);
}
.feature-icon {
font-size: 2rem;
margin-bottom: 15px;
}
.feature-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 10px;
}
.feature-desc {
font-size: 0.9rem;
color: var(--text-secondary);
}
.shortcut-table code {
background: var(--bg-tertiary);
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-get { background: var(--success); color: #000; }
.badge-post { background: var(--accent); color: #fff; }
.badge-required { background: var(--error); color: #fff; }
.badge-optional { background: var(--text-muted); color: #fff; }
.method-block {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
margin: 25px 0;
overflow: hidden;
}
.method-header {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 20px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.method-name {
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
font-weight: 600;
}
.method-body {
padding: 20px;
}
.param-table {
margin: 15px 0;
}
.param-table th {
background: var(--bg-primary);
}
.example-tabs {
display: flex;
gap: 5px;
margin-bottom: -1px;
}
.example-tab {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-bottom: none;
border-radius: 6px 6px 0 0;
cursor: pointer;
font-size: 0.85rem;
color: var(--text-secondary);
transition: all 0.2s;
}
.example-tab:hover,
.example-tab.active {
background: var(--bg-code);
color: var(--text-primary);
}
.example-content {
display: none;
}
.example-content.active {
display: block;
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin: 20px 0;
border-left: 4px solid;
}
.alert-info {
background: rgba(74, 144, 217, 0.1);
border-color: var(--accent);
}
.alert-warning {
background: rgba(237, 137, 54, 0.1);
border-color: var(--warning);
}
.alert-success {
background: rgba(72, 187, 120, 0.1);
border-color: var(--success);
}
.toc {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.toc-title {
font-weight: 600;
margin-bottom: 10px;
color: var(--text-primary);
}
.toc-list {
list-style: none;
margin: 0;
padding: 0;
}
.toc-list li {
margin: 5px 0;
}
.toc-list a {
color: var(--text-secondary);
font-size: 0.9rem;
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
}
.copy-btn:hover {
background: var(--accent);
color: #fff;
}
.code-block {
position: relative;
}
.search-box {
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.search-input {
width: 100%;
padding: 10px 15px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.9rem;
}
.search-input:focus {
outline: none;
border-color: var(--accent);
}
.search-input::placeholder {
color: var(--text-muted);
}
footer {
margin-top: 60px;
padding: 30px 0;
border-top: 1px solid var(--border-color);
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.content {
padding: 20px;
}
.mobile-menu-btn {
display: block;
position: fixed;
top: 15px;
left: 15px;
z-index: 200;
padding: 10px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
}
}
@media (min-width: 769px) {
.mobile-menu-btn {
display: none;
}
}
.hero {
text-align: center;
padding: 60px 0;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 20px;
}
.hero .tagline {
font-size: 1.3rem;
color: var(--text-secondary);
margin-bottom: 30px;
}
.btn {
display: inline-block;
padding: 12px 24px;
background: var(--accent);
color: #fff;
border-radius: 6px;
font-weight: 600;
transition: background 0.2s;
}
.btn:hover {
background: var(--accent-hover);
color: #fff;
}
.btn-outline {
background: transparent;
border: 2px solid var(--accent);
color: var(--accent);
}
.btn-outline:hover {
background: var(--accent);
color: #fff;
}
.btn-group {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.quick-start {
background: var(--bg-secondary);
border-radius: 8px;
padding: 30px;
margin: 40px 0;
}
.quick-start h3 {
margin-top: 0;
}
.step {
display: flex;
align-items: flex-start;
gap: 15px;
margin: 20px 0;
}
.step-num {
width: 30px;
height: 30px;
background: var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.anchor {
color: var(--text-muted);
margin-left: 8px;
opacity: 0;
transition: opacity 0.2s;
}
h2:hover .anchor,
h3:hover .anchor {
opacity: 1;
}

298
manual/features.html Normal file
View File

@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Features - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link active">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Features</h1>
<p class="lead">Comprehensive overview of DWN capabilities</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#window-management">Window Management</a></li>
<li><a href="#workspaces">Virtual Workspaces</a></li>
<li><a href="#layouts">Layout System</a></li>
<li><a href="#panels">Panels & System Tray</a></li>
<li><a href="#notifications">Notifications</a></li>
<li><a href="#screenshot-ocr">Screenshot & OCR</a></li>
<li><a href="#ai">AI Integration</a></li>
<li><a href="#api">WebSocket API</a></li>
</ul>
</div>
<h2 id="window-management">Window Management</h2>
<p>DWN provides comprehensive window management with both manual and automatic control.</p>
<h3>Window States</h3>
<ul>
<li><strong>Normal</strong> - Standard window state, subject to layout rules</li>
<li><strong>Floating</strong> - Exempt from tiling, freely movable and resizable</li>
<li><strong>Maximized</strong> - Fills usable area with decorations</li>
<li><strong>Fullscreen</strong> - Fills entire screen, no decorations or panels</li>
<li><strong>Minimized</strong> - Hidden from view, accessible via taskbar</li>
</ul>
<h3>Window Snapping</h3>
<p>Snap windows to screen edges with <code>Super+Arrow</code> keys. Snapping is composable:</p>
<ul>
<li><code>Super+Left</code> - Left half (press twice for full width)</li>
<li><code>Super+Right</code> - Right half</li>
<li><code>Super+Up</code> - Top half</li>
<li><code>Super+Down</code> - Bottom half</li>
<li>Combine for quarter-screen: <code>Super+Left</code> then <code>Super+Up</code></li>
</ul>
<h3>Focus Modes</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Mode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>click</code></td>
<td>Focus window on mouse click (default)</td>
</tr>
<tr>
<td><code>follow</code></td>
<td>Focus follows mouse pointer with configurable delay</td>
</tr>
</tbody>
</table>
</div>
<h3>Alt-Tab Window Cycling</h3>
<p>DWN maintains a Most Recently Used (MRU) stack per workspace. <code>Alt+Tab</code> cycles through windows in order of recent use, not visual order.</p>
<h2 id="workspaces">Virtual Workspaces</h2>
<p>DWN provides 9 virtual workspaces for organizing your windows.</p>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-title">Independent Layouts</div>
<div class="feature-desc">Each workspace maintains its own layout mode, master ratio, and master count.</div>
</div>
<div class="feature-card">
<div class="feature-title">Quick Switching</div>
<div class="feature-desc">Switch with F1-F9, or Ctrl+Alt+Left/Right for sequential navigation.</div>
</div>
<div class="feature-card">
<div class="feature-title">Window Movement</div>
<div class="feature-desc">Move windows between workspaces with Shift+F1-F9.</div>
</div>
<div class="feature-card">
<div class="feature-title">Workspace Indicator</div>
<div class="feature-desc">Panel shows which workspaces have windows and current workspace.</div>
</div>
</div>
<h2 id="layouts">Layout System</h2>
<p>Three layout modes available per workspace:</p>
<h3>Tiling Layout</h3>
<p>Master-stack tiling with configurable ratios. The first window(s) occupy the master area, others stack on the side.</p>
<ul>
<li><code>Super+H/L</code> - Adjust master area size</li>
<li><code>Super+I/D</code> - Adjust master window count</li>
<li>Default master ratio: 55%</li>
</ul>
<h3>Floating Layout</h3>
<p>Traditional floating window management. Drag title bars to move, drag edges to resize.</p>
<h3>Monocle Layout</h3>
<p>All windows fullscreen and stacked. Use Alt-Tab to switch between them. Ideal for focused work.</p>
<p>See <a href="layouts.html">Layouts</a> for detailed documentation.</p>
<h2 id="panels">Panels & System Tray</h2>
<h3>Top Panel</h3>
<p>Contains workspace indicators, taskbar, layout indicator, and system tray.</p>
<h3>Bottom Panel</h3>
<p>Contains clock and news ticker (when configured).</p>
<h3>System Tray</h3>
<p>Full XEmbed protocol support for external application icons plus built-in widgets:</p>
<ul>
<li><strong>Battery</strong> - Percentage display with charging indicator</li>
<li><strong>Volume</strong> - Click for slider, scroll to adjust, right-click to mute</li>
<li><strong>WiFi</strong> - SSID display, click for network list</li>
<li><strong>External Icons</strong> - nm-applet, blueman, Telegram, etc.</li>
</ul>
<h2 id="notifications">Notifications</h2>
<p>DWN includes a built-in D-Bus notification daemon implementing the freedesktop.org specification.</p>
<ul>
<li>Automatic service registration on startup</li>
<li>Configurable colors and timeout</li>
<li>Stacking notification display</li>
<li>Urgency level support (low, normal, critical)</li>
</ul>
<p>No external notification daemon required.</p>
<h2 id="screenshot-ocr">Screenshot & OCR</h2>
<p>Built-in screenshot capture and OCR text extraction, accessible via the WebSocket API.</p>
<h3>Screenshot Modes</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Mode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>fullscreen</code></td>
<td>Capture entire screen</td>
</tr>
<tr>
<td><code>window</code></td>
<td>Capture specific window by ID</td>
</tr>
<tr>
<td><code>active</code></td>
<td>Capture currently focused window</td>
</tr>
<tr>
<td><code>area</code></td>
<td>Capture arbitrary rectangle</td>
</tr>
</tbody>
</table>
</div>
<h3>OCR Text Extraction</h3>
<p>Extract text from screenshots using Tesseract OCR engine:</p>
<ul>
<li>Multi-language support (English by default)</li>
<li>Confidence score reporting</li>
<li>Useful for automation and accessibility</li>
</ul>
<p>See <a href="api-reference.html">API Reference</a> for usage details.</p>
<h2 id="ai">AI Integration</h2>
<p>Optional AI features powered by OpenRouter API:</p>
<h3>AI Command Palette</h3>
<p>Natural language command input (<code>Super+Shift+A</code>). Ask the AI to:</p>
<ul>
<li>Launch applications ("open firefox")</li>
<li>Answer questions</li>
<li>Get system information</li>
</ul>
<h3>AI Context Analysis</h3>
<p>Press <code>Super+A</code> to see AI analysis of your current task based on open windows.</p>
<h3>Exa Semantic Search</h3>
<p>Web search with semantic understanding (<code>Super+Shift+E</code>). Find documentation, tutorials, and resources.</p>
<p>See <a href="ai-features.html">AI Integration</a> for setup instructions.</p>
<h2 id="api">WebSocket API</h2>
<p>Full programmatic control via JSON WebSocket API on configurable port (default 8777).</p>
<h3>Capabilities</h3>
<ul>
<li>Window management (list, focus, move, resize, close)</li>
<li>Workspace control (switch, move windows)</li>
<li>Layout management (mode, master ratio)</li>
<li>Keyboard simulation (key presses, typing)</li>
<li>Mouse simulation (move, click, drag)</li>
<li>Screenshot capture and OCR</li>
<li>System queries (windows, workspaces, monitors)</li>
</ul>
<h3>Use Cases</h3>
<ul>
<li>Test automation</li>
<li>Custom hotkey scripts</li>
<li>Remote desktop control</li>
<li>Accessibility tools</li>
<li>Integration with other applications</li>
</ul>
<p>See <a href="api-overview.html">API Overview</a> for getting started.</p>
<h2>Protocol Compliance</h2>
<p>DWN implements standard X11 protocols for compatibility:</p>
<ul>
<li><strong>EWMH</strong> - Extended Window Manager Hints</li>
<li><strong>ICCCM</strong> - Inter-Client Communication Conventions</li>
<li><strong>XEmbed</strong> - System tray embedding protocol</li>
<li><strong>Xinerama</strong> - Multi-monitor support</li>
</ul>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

168
manual/index.html Normal file
View File

@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DWN - Desktop Window Manager Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link active">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="hero">
<h1>DWN</h1>
<p class="tagline">A modern X11 window manager with AI integration and WebSocket API</p>
<div class="btn-group">
<a href="installation.html" class="btn">Get Started</a>
<a href="api-overview.html" class="btn btn-outline">API Docs</a>
</div>
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🪟</div>
<div class="feature-title">Multiple Layouts</div>
<div class="feature-desc">Tiling, floating, and monocle layouts with per-workspace configuration. Master-stack tiling with adjustable ratios.</div>
</div>
<div class="feature-card">
<div class="feature-icon">🖥️</div>
<div class="feature-title">9 Workspaces</div>
<div class="feature-desc">Virtual desktops with independent layout settings. Quick switching with F1-F9 keys.</div>
</div>
<div class="feature-card">
<div class="feature-icon">🤖</div>
<div class="feature-title">AI Integration</div>
<div class="feature-desc">OpenRouter API for intelligent command palette and Exa semantic search integration.</div>
</div>
<div class="feature-card">
<div class="feature-icon">🔌</div>
<div class="feature-title">WebSocket API</div>
<div class="feature-desc">Full programmatic control via JSON WebSocket API. Automate everything.</div>
</div>
<div class="feature-card">
<div class="feature-icon">📸</div>
<div class="feature-title">Screenshot & OCR</div>
<div class="feature-desc">Capture screenshots and extract text with Tesseract OCR, all via API.</div>
</div>
<div class="feature-card">
<div class="feature-icon">🔔</div>
<div class="feature-title">Notifications</div>
<div class="feature-desc">Built-in D-Bus notification daemon with configurable appearance.</div>
</div>
</div>
<div class="quick-start">
<h3>Quick Start</h3>
<div class="step">
<div class="step-num">1</div>
<div class="step-content">
<strong>Install dependencies</strong>
<pre><code>make deps</code></pre>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-content">
<strong>Build DWN</strong>
<pre><code>make</code></pre>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-content">
<strong>Test in Xephyr</strong>
<pre><code>make run</code></pre>
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-content">
<strong>Install system-wide</strong>
<pre><code>sudo make install</code></pre>
</div>
</div>
</div>
<h2>Key Features</h2>
<h3>Window Management</h3>
<ul>
<li>Three layout modes: tiling, floating, and monocle</li>
<li>Window snapping with Super+Arrow keys</li>
<li>Alt-Tab window cycling with MRU (Most Recently Used) stack</li>
<li>EWMH/ICCCM protocol compliance</li>
<li>Multi-monitor support via Xinerama</li>
</ul>
<h3>Automation API</h3>
<ul>
<li>WebSocket JSON API on configurable port</li>
<li>Keyboard and mouse simulation</li>
<li>Window management and layout control</li>
<li>Screenshot capture and OCR text extraction</li>
<li>Python client library included</li>
</ul>
<h3>System Integration</h3>
<ul>
<li>XDG autostart support</li>
<li>System tray with XEmbed protocol</li>
<li>D-Bus notification daemon</li>
<li>Configurable panels (top and bottom)</li>
</ul>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

240
manual/installation.html Normal file
View File

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Installation - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Installation</h1>
<p class="lead">Install DWN on your Linux system</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#requirements">Requirements</a></li>
<li><a href="#dependencies">Dependencies</a></li>
<li><a href="#building">Building</a></li>
<li><a href="#installing">Installing</a></li>
<li><a href="#testing">Testing</a></li>
</ul>
</div>
<h2 id="requirements">Requirements</h2>
<ul>
<li>Linux with X11 (Xorg)</li>
<li>GCC compiler</li>
<li>Make build system</li>
<li>pkg-config</li>
</ul>
<h2 id="dependencies">Dependencies</h2>
<h3>Ubuntu / Debian</h3>
<div class="code-block">
<pre><code>sudo apt update && sudo apt install -y \
build-essential \
pkg-config \
libx11-dev \
libxext-dev \
libxinerama-dev \
libxrandr-dev \
libxft-dev \
libxtst-dev \
libfontconfig1-dev \
libdbus-1-dev \
libcurl4-openssl-dev \
libpng-dev \
libtesseract-dev \
libleptonica-dev \
tesseract-ocr \
tesseract-ocr-eng \
xserver-xephyr \
dmenu</code></pre>
</div>
<h3>Fedora / RHEL</h3>
<div class="code-block">
<pre><code>sudo dnf install -y \
gcc make \
pkg-config \
libX11-devel \
libXext-devel \
libXinerama-devel \
libXrandr-devel \
libXtst-devel \
dbus-devel \
libcurl-devel \
libpng-devel \
tesseract-devel \
leptonica-devel \
tesseract-langpack-eng \
xorg-x11-server-Xephyr \
dmenu</code></pre>
</div>
<h3>Arch Linux</h3>
<div class="code-block">
<pre><code>sudo pacman -S --needed \
base-devel \
pkg-config \
libx11 \
libxext \
libxinerama \
libxrandr \
libxtst \
dbus \
curl \
libpng \
tesseract \
tesseract-data-eng \
leptonica \
xorg-server-xephyr \
dmenu</code></pre>
</div>
<h3>Automatic Installation</h3>
<p>The Makefile can automatically detect your package manager and install dependencies:</p>
<div class="code-block">
<pre><code>make deps</code></pre>
</div>
<h2 id="building">Building</h2>
<h3>Clone the Repository</h3>
<div class="code-block">
<pre><code>git clone https://github.com/retoor/dwn.git
cd dwn</code></pre>
</div>
<h3>Build Release Version</h3>
<div class="code-block">
<pre><code>make</code></pre>
</div>
<h3>Build Debug Version</h3>
<div class="code-block">
<pre><code>make debug</code></pre>
</div>
<h3>Build with Sanitizers</h3>
<div class="code-block">
<pre><code>make sanitize</code></pre>
</div>
<h2 id="installing">Installing</h2>
<h3>System-wide Installation</h3>
<div class="code-block">
<pre><code>sudo make install</code></pre>
</div>
<p>This installs:</p>
<ul>
<li><code>/usr/local/bin/dwn</code> - The window manager binary</li>
<li><code>/usr/local/share/xsessions/dwn.desktop</code> - Session file for display managers</li>
<li><code>/etc/dwn/config.example</code> - Example configuration file</li>
</ul>
<h3>User Configuration</h3>
<p>Copy the example configuration to your home directory:</p>
<div class="code-block">
<pre><code>mkdir -p ~/.config/dwn
cp /etc/dwn/config.example ~/.config/dwn/config</code></pre>
</div>
<h3>Uninstalling</h3>
<div class="code-block">
<pre><code>sudo make uninstall</code></pre>
</div>
<h2 id="testing">Testing</h2>
<h3>Test in Xephyr (Recommended)</h3>
<p>Test DWN in a nested X server without affecting your current session:</p>
<div class="code-block">
<pre><code>make run</code></pre>
</div>
<p>This starts DWN in a 1280x720 Xephyr window on display :1.</p>
<h3>Manual Testing</h3>
<div class="code-block">
<pre><code># Start Xephyr
Xephyr :1 -screen 1920x1080 &
# Run DWN on the new display
DISPLAY=:1 ./bin/dwn</code></pre>
</div>
<h3>Using DWN as Your Window Manager</h3>
<ol>
<li>Log out of your current session</li>
<li>At the login screen, select "DWN" from the session menu</li>
<li>Log in</li>
</ol>
<div class="alert alert-info">
<strong>Tip:</strong> Keep a terminal open or know the shortcut <code>Ctrl+Alt+T</code> to open one, in case you need to recover from any issues.
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

119
manual/js/main.js Normal file
View File

@ -0,0 +1,119 @@
/* retoor <retoor@molodetz.nl> */
/* DWN Documentation Scripts */
document.addEventListener('DOMContentLoaded', function() {
highlightCurrentPage();
setupCopyButtons();
setupMobileMenu();
setupSearch();
setupTabs();
});
function highlightCurrentPage() {
const currentPath = window.location.pathname.split('/').pop() || 'index.html';
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
const href = link.getAttribute('href');
if (href === currentPath || (currentPath === '' && href === 'index.html')) {
link.classList.add('active');
}
});
}
function setupCopyButtons() {
const codeBlocks = document.querySelectorAll('pre code');
codeBlocks.forEach(block => {
const wrapper = block.closest('.code-block') || block.parentElement;
wrapper.style.position = 'relative';
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = 'Copy';
btn.addEventListener('click', function() {
const text = block.textContent;
navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy';
}, 2000);
});
});
wrapper.appendChild(btn);
});
}
function setupMobileMenu() {
const menuBtn = document.querySelector('.mobile-menu-btn');
const sidebar = document.querySelector('.sidebar');
if (menuBtn && sidebar) {
menuBtn.addEventListener('click', function() {
sidebar.classList.toggle('open');
});
document.addEventListener('click', function(e) {
if (!sidebar.contains(e.target) && !menuBtn.contains(e.target)) {
sidebar.classList.remove('open');
}
});
}
}
function setupSearch() {
const searchInput = document.querySelector('.search-input');
if (!searchInput) return;
searchInput.addEventListener('input', function(e) {
const query = e.target.value.toLowerCase();
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
const text = link.textContent.toLowerCase();
const section = link.closest('.nav-section');
if (query === '' || text.includes(query)) {
link.style.display = 'block';
} else {
link.style.display = 'none';
}
});
});
}
function setupTabs() {
const tabGroups = document.querySelectorAll('.example-tabs');
tabGroups.forEach(group => {
const tabs = group.querySelectorAll('.example-tab');
const container = group.nextElementSibling;
tabs.forEach(tab => {
tab.addEventListener('click', function() {
const target = this.dataset.tab;
tabs.forEach(t => t.classList.remove('active'));
this.classList.add('active');
if (container) {
const contents = container.querySelectorAll('.example-content');
contents.forEach(c => {
c.classList.remove('active');
if (c.dataset.tab === target) {
c.classList.add('active');
}
});
}
});
});
});
}
function scrollToSection(id) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}

298
manual/layouts.html Normal file
View File

@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Layouts - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link active">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Layouts</h1>
<p class="lead">Understanding DWN's window layout system</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#overview">Overview</a></li>
<li><a href="#tiling">Tiling Layout</a></li>
<li><a href="#floating">Floating Layout</a></li>
<li><a href="#monocle">Monocle Layout</a></li>
<li><a href="#per-workspace">Per-Workspace Settings</a></li>
<li><a href="#shortcuts">Layout Shortcuts</a></li>
</ul>
</div>
<h2 id="overview">Overview</h2>
<p>DWN provides three layout modes that determine how windows are arranged on screen. Each workspace maintains its own layout settings independently.</p>
<p>Press <code>Super+Space</code> to cycle through layouts: Tiling → Floating → Monocle → Tiling</p>
<div class="alert alert-info">
<strong>Tip:</strong> The current layout is shown in the panel with a symbol: <code>[]=</code> for tiling, <code>&gt;&lt;&gt;</code> for floating, <code>[M]</code> for monocle.
</div>
<h2 id="tiling">Tiling Layout</h2>
<p>The default layout. Windows automatically arrange themselves in a master-stack pattern.</p>
<h3>How It Works</h3>
<div class="code-block">
<pre><code>+------------------+----------+
| | Window |
| Master | 2 |
| Window +----------+
| 1 | Window |
| | 3 |
+------------------+----------+</code></pre>
</div>
<ul>
<li>The <strong>master area</strong> (left) contains the primary window(s)</li>
<li>The <strong>stack area</strong> (right) contains secondary windows</li>
<li>Windows are automatically sized to fill the screen</li>
<li>No manual positioning required</li>
</ul>
<h3>Master Ratio</h3>
<p>Controls the width of the master area relative to the screen.</p>
<ul>
<li><code>Super+H</code> - Decrease master ratio (shrink master area)</li>
<li><code>Super+L</code> - Increase master ratio (expand master area)</li>
<li>Range: 10% to 90%</li>
<li>Default: 55%</li>
</ul>
<h3>Master Count</h3>
<p>Controls how many windows occupy the master area.</p>
<ul>
<li><code>Super+I</code> - Increase master count</li>
<li><code>Super+D</code> - Decrease master count</li>
<li>Range: 1 to 10</li>
<li>Default: 1</li>
</ul>
<p>With master count of 2:</p>
<div class="code-block">
<pre><code>+--------+---------+----------+
| Master | Master | Stack |
| 1 | 2 | 3 |
| | +----------+
| | | Stack |
| | | 4 |
+--------+---------+----------+</code></pre>
</div>
<h3>Floating Windows in Tiling</h3>
<p>Press <code>Super+F9</code> to toggle floating mode for a window. Floating windows:</p>
<ul>
<li>Are exempt from automatic tiling</li>
<li>Can be freely moved and resized</li>
<li>Float above tiled windows</li>
<li>Useful for dialogs, small utilities, reference windows</li>
</ul>
<h2 id="floating">Floating Layout</h2>
<p>Traditional overlapping window management like most desktop environments.</p>
<h3>Characteristics</h3>
<ul>
<li>Windows maintain their own position and size</li>
<li>No automatic arrangement</li>
<li>Drag title bars to move windows</li>
<li>Drag window edges to resize</li>
<li>Click to raise windows</li>
</ul>
<h3>Window Controls</h3>
<p>In floating mode, windows have full manual control:</p>
<ul>
<li><strong>Move</strong>: Drag the title bar</li>
<li><strong>Resize</strong>: Drag the window border</li>
<li><strong>Maximize</strong>: <code>Alt+F10</code> or click maximize button</li>
<li><strong>Minimize</strong>: <code>Alt+F9</code> or click minimize button</li>
</ul>
<h2 id="monocle">Monocle Layout</h2>
<p>Each window takes the full screen. Only one window visible at a time.</p>
<h3>Use Cases</h3>
<ul>
<li>Focused work on a single application</li>
<li>Small screens or high window count</li>
<li>Reading or writing documents</li>
<li>Video or media playback</li>
</ul>
<h3>Navigation</h3>
<ul>
<li><code>Alt+Tab</code> - Switch to next window</li>
<li><code>Alt+Shift+Tab</code> - Switch to previous window</li>
<li>Taskbar click - Switch to specific window</li>
</ul>
<div class="alert alert-info">
<strong>Note:</strong> Windows maintain decorations in monocle mode unless fullscreen (<code>Alt+F11</code>).
</div>
<h2 id="per-workspace">Per-Workspace Settings</h2>
<p>Each workspace maintains independent layout settings:</p>
<div class="table-container">
<table>
<thead>
<tr>
<th>Setting</th>
<th>Scope</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Layout Mode</td>
<td>Per-workspace</td>
<td>Tiling, floating, or monocle</td>
</tr>
<tr>
<td>Master Ratio</td>
<td>Per-workspace</td>
<td>Width of master area</td>
</tr>
<tr>
<td>Master Count</td>
<td>Per-workspace</td>
<td>Windows in master area</td>
</tr>
<tr>
<td>Window Floating State</td>
<td>Per-window</td>
<td>Individual floating toggle</td>
</tr>
</tbody>
</table>
</div>
<p>Example workflow:</p>
<ul>
<li>Workspace 1: Tiling layout for coding (editor + terminal)</li>
<li>Workspace 2: Floating layout for design work</li>
<li>Workspace 3: Monocle layout for focused writing</li>
<li>Workspace 4: Tiling with master count 2 for comparison</li>
</ul>
<h2 id="shortcuts">Layout Shortcuts</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+Space</code></td>
<td>Cycle layout mode</td>
</tr>
<tr>
<td><code>Super+H</code></td>
<td>Shrink master area</td>
</tr>
<tr>
<td><code>Super+L</code></td>
<td>Expand master area</td>
</tr>
<tr>
<td><code>Super+I</code></td>
<td>Increase master count</td>
</tr>
<tr>
<td><code>Super+D</code></td>
<td>Decrease master count</td>
</tr>
<tr>
<td><code>Super+F9</code></td>
<td>Toggle window floating</td>
</tr>
<tr>
<td><code>Alt+F10</code></td>
<td>Toggle maximize</td>
</tr>
<tr>
<td><code>Alt+F11</code></td>
<td>Toggle fullscreen</td>
</tr>
</tbody>
</table>
</div>
<h2>Configuration</h2>
<p>Set default layout behavior in <code>~/.config/dwn/config</code>:</p>
<div class="code-block">
<pre><code>[layout]
default = tiling # Default layout for new workspaces
master_ratio = 0.55 # Default master area ratio (0.1-0.9)
master_count = 1 # Default master window count (1-10)
[appearance]
gap = 5 # Gap between tiled windows (0-100)</code></pre>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

241
manual/quickstart.html Normal file
View File

@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quick Start - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Quick Start</h1>
<p class="lead">Get up and running with DWN in 5 minutes</p>
</div>
<h2>Essential Shortcuts</h2>
<p>These are the most important shortcuts to know:</p>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Ctrl+Alt+T</code></td>
<td>Open terminal</td>
</tr>
<tr>
<td><code>Super</code> or <code>Alt+F2</code></td>
<td>Open application launcher (dmenu)</td>
</tr>
<tr>
<td><code>Alt+F4</code></td>
<td>Close focused window</td>
</tr>
<tr>
<td><code>Alt+Tab</code></td>
<td>Cycle through windows</td>
</tr>
<tr>
<td><code>F1</code> - <code>F9</code></td>
<td>Switch to workspace 1-9</td>
</tr>
<tr>
<td><code>Super+Space</code></td>
<td>Cycle layout mode</td>
</tr>
<tr>
<td><code>Super+Backspace</code></td>
<td>Quit DWN</td>
</tr>
</tbody>
</table>
</div>
<h2>Your First Session</h2>
<h3>1. Launch Applications</h3>
<p>Press <code>Super</code> (Windows key) to open the application launcher. Type the name of the application and press Enter.</p>
<h3>2. Navigate Workspaces</h3>
<p>DWN provides 9 virtual workspaces. Use <code>F1</code> through <code>F9</code> to switch between them. The panel at the top shows which workspaces have windows.</p>
<h3>3. Manage Windows</h3>
<p>By default, DWN uses a tiling layout where windows automatically arrange themselves. The first window takes the "master" area (left side), and subsequent windows stack on the right.</p>
<h3>4. Move Windows Between Workspaces</h3>
<p>Press <code>Shift+F1</code> through <code>Shift+F9</code> to move the focused window to a different workspace.</p>
<h2>Layout Modes</h2>
<p>Press <code>Super+Space</code> to cycle through layout modes:</p>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-title">Tiling</div>
<div class="feature-desc">Master-stack layout. First window on the left, others stacked on the right. Use <code>Super+H/L</code> to resize the master area.</div>
</div>
<div class="feature-card">
<div class="feature-title">Floating</div>
<div class="feature-desc">Traditional floating windows. Drag title bars to move, drag edges to resize.</div>
</div>
<div class="feature-card">
<div class="feature-title">Monocle</div>
<div class="feature-desc">All windows fullscreen, stacked. Use Alt+Tab to switch between them.</div>
</div>
</div>
<h2>Window Snapping</h2>
<p>Use <code>Super+Arrow</code> keys to snap windows:</p>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+Left</code></td>
<td>Snap to left half (press twice for full width)</td>
</tr>
<tr>
<td><code>Super+Right</code></td>
<td>Snap to right half</td>
</tr>
<tr>
<td><code>Super+Up</code></td>
<td>Snap to top half</td>
</tr>
<tr>
<td><code>Super+Down</code></td>
<td>Snap to bottom half</td>
</tr>
</tbody>
</table>
</div>
<p>Snapping is composable: <code>Super+Left</code> then <code>Super+Up</code> snaps to the top-left quarter.</p>
<h2>Window States</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Alt+F9</code></td>
<td>Minimize window</td>
</tr>
<tr>
<td><code>Alt+F10</code></td>
<td>Maximize window</td>
</tr>
<tr>
<td><code>Alt+F11</code></td>
<td>Fullscreen (no decorations)</td>
</tr>
<tr>
<td><code>Super+F9</code></td>
<td>Toggle floating mode</td>
</tr>
</tbody>
</table>
</div>
<h2>Configuration</h2>
<p>DWN reads configuration from <code>~/.config/dwn/config</code>. Create one from the example:</p>
<div class="code-block">
<pre><code>mkdir -p ~/.config/dwn
cp /etc/dwn/config.example ~/.config/dwn/config</code></pre>
</div>
<p>Edit the file to customize terminal, launcher, colors, and more. See the <a href="configuration.html">Configuration</a> page for details.</p>
<h2>Enable the API</h2>
<p>To enable the WebSocket API for automation, add this to your config:</p>
<div class="code-block">
<pre><code>[api]
enabled = true
port = 8777</code></pre>
</div>
<p>Then you can control DWN programmatically. See <a href="api-overview.html">API Overview</a>.</p>
<h2>Next Steps</h2>
<ul>
<li><a href="shortcuts.html">Full keyboard shortcuts reference</a></li>
<li><a href="configuration.html">Configuration options</a></li>
<li><a href="ai-features.html">AI integration setup</a></li>
<li><a href="api-overview.html">API documentation</a></li>
</ul>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

326
manual/shortcuts.html Normal file
View File

@ -0,0 +1,326 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyboard Shortcuts - DWN Documentation</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<button class="mobile-menu-btn">Menu</button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>DWN</h1>
<span class="version">v1.0.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search docs...">
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Getting Started</div>
<a href="index.html" class="nav-link">Introduction</a>
<a href="installation.html" class="nav-link">Installation</a>
<a href="quickstart.html" class="nav-link">Quick Start</a>
</div>
<div class="nav-section">
<div class="nav-section-title">User Guide</div>
<a href="features.html" class="nav-link">Features</a>
<a href="shortcuts.html" class="nav-link">Keyboard Shortcuts</a>
<a href="configuration.html" class="nav-link">Configuration</a>
<a href="layouts.html" class="nav-link">Layouts</a>
<a href="ai-features.html" class="nav-link">AI Integration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">API Reference</div>
<a href="api-overview.html" class="nav-link">API Overview</a>
<a href="api-reference.html" class="nav-link">API Reference</a>
<a href="api-examples.html" class="nav-link">API Examples</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Advanced</div>
<a href="architecture.html" class="nav-link">Architecture</a>
<a href="building.html" class="nav-link">Building from Source</a>
</div>
</nav>
</aside>
<main class="main-content">
<div class="content">
<div class="page-header">
<h1>Keyboard Shortcuts</h1>
<p class="lead">Complete reference of all keyboard shortcuts</p>
</div>
<div class="toc">
<div class="toc-title">On this page</div>
<ul class="toc-list">
<li><a href="#launchers">Application Launchers</a></li>
<li><a href="#windows">Window Management</a></li>
<li><a href="#workspaces">Workspace Navigation</a></li>
<li><a href="#layouts">Layout Control</a></li>
<li><a href="#snapping">Window Snapping</a></li>
<li><a href="#ai">AI Features</a></li>
<li><a href="#system">Help & System</a></li>
</ul>
</div>
<h2 id="launchers">Application Launchers</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Ctrl+Alt+T</code></td>
<td>Open terminal (configurable)</td>
</tr>
<tr>
<td><code>Super</code> / <code>Alt+F2</code></td>
<td>Open application launcher (dmenu/rofi)</td>
</tr>
<tr>
<td><code>Super+E</code></td>
<td>Open file manager (configurable)</td>
</tr>
<tr>
<td><code>Super+B</code></td>
<td>Open web browser</td>
</tr>
<tr>
<td><code>Print</code></td>
<td>Take screenshot (xfce4-screenshooter)</td>
</tr>
</tbody>
</table>
</div>
<h2 id="windows">Window Management</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Alt+F4</code></td>
<td>Close focused window</td>
</tr>
<tr>
<td><code>Alt+Tab</code></td>
<td>Cycle to next window (MRU order)</td>
</tr>
<tr>
<td><code>Alt+Shift+Tab</code></td>
<td>Cycle to previous window</td>
</tr>
<tr>
<td><code>Alt+F9</code></td>
<td>Toggle minimize/restore</td>
</tr>
<tr>
<td><code>Alt+F10</code></td>
<td>Toggle maximize</td>
</tr>
<tr>
<td><code>Alt+F11</code></td>
<td>Toggle fullscreen (no decorations)</td>
</tr>
<tr>
<td><code>Super+F9</code></td>
<td>Toggle floating mode for focused window</td>
</tr>
</tbody>
</table>
</div>
<h2 id="workspaces">Workspace Navigation</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>F1</code> - <code>F9</code></td>
<td>Switch to workspace 1-9</td>
</tr>
<tr>
<td><code>Shift+F1</code> - <code>Shift+F9</code></td>
<td>Move focused window to workspace 1-9</td>
</tr>
<tr>
<td><code>Ctrl+Alt+Right</code></td>
<td>Switch to next workspace</td>
</tr>
<tr>
<td><code>Ctrl+Alt+Left</code></td>
<td>Switch to previous workspace</td>
</tr>
</tbody>
</table>
</div>
<h2 id="layouts">Layout Control</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+Space</code></td>
<td>Cycle layout mode (tiling → floating → monocle)</td>
</tr>
<tr>
<td><code>Super+H</code></td>
<td>Shrink master area</td>
</tr>
<tr>
<td><code>Super+L</code></td>
<td>Expand master area</td>
</tr>
<tr>
<td><code>Super+I</code></td>
<td>Increase master window count</td>
</tr>
<tr>
<td><code>Super+D</code></td>
<td>Decrease master window count</td>
</tr>
</tbody>
</table>
</div>
<h2 id="snapping">Window Snapping</h2>
<p>Window snapping is composable. Press the same key twice to expand to full width/height in that direction.</p>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+Left</code></td>
<td>Snap left 50% (press twice for full width)</td>
</tr>
<tr>
<td><code>Super+Right</code></td>
<td>Snap right 50% (press twice for full width)</td>
</tr>
<tr>
<td><code>Super+Up</code></td>
<td>Snap top 50% (press twice for full height)</td>
</tr>
<tr>
<td><code>Super+Down</code></td>
<td>Snap bottom 50% (press twice for full height)</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info">
<strong>Tip:</strong> Combine horizontal and vertical snaps for quarter-screen windows. For example, <code>Super+Left</code> then <code>Super+Up</code> snaps to top-left quarter.
</div>
<h2 id="ai">AI Features</h2>
<p>These shortcuts require API keys to be configured. See <a href="ai-features.html">AI Integration</a>.</p>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+A</code></td>
<td>Show AI context analysis</td>
</tr>
<tr>
<td><code>Super+Shift+A</code></td>
<td>Open AI command palette</td>
</tr>
<tr>
<td><code>Super+Shift+E</code></td>
<td>Open Exa semantic web search</td>
</tr>
</tbody>
</table>
</div>
<h2 id="system">Help & System</h2>
<div class="table-container">
<table class="shortcut-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Super+S</code></td>
<td>Show all keyboard shortcuts</td>
</tr>
<tr>
<td><code>Super+T</code></td>
<td>Start/continue interactive tutorial</td>
</tr>
<tr>
<td><code>Super+Shift+D</code></td>
<td>Start/stop demo mode</td>
</tr>
<tr>
<td><code>Super+D</code></td>
<td>Toggle show desktop</td>
</tr>
<tr>
<td><code>Super+Return</code></td>
<td>Open current news article in browser</td>
</tr>
<tr>
<td><code>Super+Backspace</code></td>
<td>Quit DWN</td>
</tr>
</tbody>
</table>
</div>
<footer>
<p>DWN Window Manager - retoor &lt;retoor@molodetz.nl&gt;</p>
</footer>
</div>
</main>
</div>
<script src="js/main.js"></script>
</body>
</html>

1823
src/api.c

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
#include "notifications.h"
#include "layout.h"
#include "panel.h"
#include "api.h"
#include <string.h>
#include <stdlib.h>
@ -234,6 +235,8 @@ Client *client_manage(Window window)
client_sync_log("client_manage: focusing");
client_focus(client, true);
api_emit_window_created(client->window, client->title, client->class, client->workspace);
client_sync_log("client_manage: DONE");
return client;
}
@ -247,6 +250,8 @@ void client_unmanage(Client *client)
return;
}
api_emit_window_destroyed(client->window, client->title, client->workspace);
LOG_DEBUG("Unmanaging window: %lu", client->window);
if (dwn->alt_tab_client == client) {
@ -345,7 +350,9 @@ void client_focus(Client *client, bool update_mru)
client_sync_log("client_focus: unfocusing previous");
Workspace *ws = workspace_get(client->workspace);
Window prev_window = None;
if (ws != NULL && ws->focused != NULL && ws->focused != client) {
prev_window = ws->focused->window;
client_unfocus(ws->focused);
}
@ -378,6 +385,8 @@ void client_focus(Client *client, bool update_mru)
atoms_set_active_window(client->window);
api_emit_window_focused(client->window, client->title, prev_window);
client_sync_log("client_focus: DONE");
LOG_DEBUG("Focused window: %s", client->title);
}
@ -395,6 +404,8 @@ void client_unfocus(Client *client)
}
decorations_render(client, false);
api_emit_window_unfocused(client->window, client->title);
}
void client_raise(Client *client)
@ -414,6 +425,8 @@ void client_raise(Client *client)
XRaiseWindow(dwn->display, win);
api_emit_window_raised(client->window);
notifications_raise_all();
panel_raise_all();
}
@ -433,6 +446,8 @@ void client_lower(Client *client)
return;
}
XLowerWindow(dwn->display, win);
api_emit_window_lowered(client->window);
}
void client_minimize(Client *client)
@ -444,6 +459,8 @@ void client_minimize(Client *client)
client->flags |= CLIENT_MINIMIZED;
client_hide(client);
api_emit_window_minimized(client->window);
Workspace *ws = workspace_get(client->workspace);
if (ws != NULL && ws->focused == client) {
Client *next = workspace_mru_get_previous(client->workspace, client);
@ -466,6 +483,8 @@ void client_restore(Client *client)
client->flags &= ~CLIENT_MINIMIZED;
client_show(client);
client_focus(client, true);
api_emit_window_restored(client->window);
}
@ -475,6 +494,9 @@ void client_move(Client *client, int x, int y)
return;
}
int old_x = client->x;
int old_y = client->y;
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
@ -505,6 +527,10 @@ void client_move(Client *client, int x, int y)
client->x = x;
client->y = y;
client_configure(client);
if (old_x != client->x || old_y != client->y) {
api_emit_window_moved(client->window, old_x, old_y, client->x, client->y);
}
}
void client_resize(Client *client, int width, int height)
@ -513,6 +539,9 @@ void client_resize(Client *client, int width, int height)
return;
}
int old_w = client->width;
int old_h = client->height;
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
@ -535,6 +564,10 @@ void client_resize(Client *client, int width, int height)
client->width = width;
client->height = height;
client_configure(client);
if (old_w != client->width || old_h != client->height) {
api_emit_window_resized(client->window, old_w, old_h, client->width, client->height);
}
}
void client_move_resize(Client *client, int x, int y, int width, int height)
@ -682,6 +715,10 @@ void client_update_title(Client *client)
return;
}
char old_title[256];
strncpy(old_title, client->title, sizeof(old_title) - 1);
old_title[sizeof(old_title) - 1] = '\0';
char *name = atoms_get_window_name(client->window);
if (name != NULL) {
strncpy(client->title, name, sizeof(client->title) - 1);
@ -691,6 +728,10 @@ void client_update_title(Client *client)
strncpy(client->title, "Untitled", sizeof(client->title) - 1);
}
if (strcmp(old_title, client->title) != 0) {
api_emit_window_title_changed(client->window, old_title, client->title);
}
if (client->frame != None) {
Workspace *ws = workspace_get(client->workspace);
bool focused = (ws != NULL && ws->focused == client);
@ -733,6 +774,7 @@ void client_set_fullscreen(Client *client, bool fullscreen)
client->flags |= CLIENT_FULLSCREEN;
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, true);
api_emit_window_fullscreen(client->window, true);
client->x = 0;
client->y = 0;
@ -753,6 +795,7 @@ void client_set_fullscreen(Client *client, bool fullscreen)
} else {
client->flags &= ~CLIENT_FULLSCREEN;
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, false);
api_emit_window_fullscreen(client->window, false);
client->x = client->old_x;
client->y = client->old_y;
@ -794,6 +837,8 @@ void client_set_floating(Client *client, bool floating)
} else {
client->flags &= ~CLIENT_FLOATING;
}
api_emit_window_floating(client->window, floating);
}
void client_toggle_floating(Client *client)
@ -838,6 +883,7 @@ void client_set_maximize(Client *client, bool maximized)
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_VERT, true);
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_HORZ, true);
api_emit_window_maximized(client->window, true);
int area_x, area_y, area_width, area_height;
layout_get_usable_area(&area_x, &area_y, &area_width, &area_height);
@ -860,6 +906,7 @@ void client_set_maximize(Client *client, bool maximized)
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_VERT, false);
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_HORZ, false);
api_emit_window_maximized(client->window, false);
client->x = client->old_x;
client->y = client->old_y;

View File

@ -16,6 +16,7 @@
#include "decorations.h"
#include "demo.h"
#include "layout.h"
#include "api.h"
#include <stdio.h>
#include <string.h>
@ -456,11 +457,17 @@ void keys_handle_press(XKeyEvent *ev)
bindings[i].modifiers == clean_mask) {
if (bindings[i].callback != NULL) {
LOG_DEBUG("Key binding triggered: %s", bindings[i].description);
const char *name = XKeysymToString(keysym);
api_emit_shortcut_triggered(name ? name : "unknown", bindings[i].description);
bindings[i].callback();
}
return;
}
}
api_emit_key_pressed(ev->keycode, keysym, clean_mask);
}
void keys_handle_release(XKeyEvent *ev)
@ -470,6 +477,9 @@ void keys_handle_release(XKeyEvent *ev)
}
KeySym keysym = XLookupKeysym(ev, 0);
unsigned int clean_mask = ev->state & ~(Mod2Mask | LockMask);
api_emit_key_released(ev->keycode, keysym, clean_mask);
if (keysym == XK_Super_L || keysym == XK_Super_R) {
if (super_pressed && !super_used_in_combo) {
@ -767,6 +777,7 @@ void key_show_desktop(void)
}
}
dwn->desktop_shown = true;
api_emit_show_desktop_toggled(true);
} else {
for (int i = 0; i < dwn->desktop_minimized_count; i++) {
Client *c = client_find_by_window(dwn->desktop_minimized[i]);
@ -776,6 +787,7 @@ void key_show_desktop(void)
}
dwn->desktop_minimized_count = 0;
dwn->desktop_shown = false;
api_emit_show_desktop_toggled(false);
}
}

View File

@ -22,6 +22,8 @@
#include "services.h"
#include "api.h"
#include "demo.h"
#include "screenshot.h"
#include "ocr.h"
#include "util.h"
#include <stdio.h>
@ -466,6 +468,8 @@ static void handle_button_press(XButtonEvent *ev)
dwn->drag_orig_y = c->y;
dwn->resizing = false;
api_emit_drag_started(c->window, false);
XGrabPointer(dwn->display, c->frame, True,
PointerMotionMask | ButtonReleaseMask,
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
@ -485,12 +489,16 @@ static void handle_button_press(XButtonEvent *ev)
dwn->drag_orig_h = c->height;
dwn->resizing = true;
api_emit_drag_started(c->window, true);
XGrabPointer(dwn->display, c->frame, True,
PointerMotionMask | ButtonReleaseMask,
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
}
}
}
api_emit_mouse_button_pressed(ev->button, ev->x_root, ev->y_root);
}
static void handle_button_release(XButtonEvent *ev)
@ -503,12 +511,13 @@ static void handle_button_release(XButtonEvent *ev)
volume_slider_handle_release(volume_slider);
}
(void)ev;
if (dwn->drag_client != NULL) {
api_emit_drag_ended(dwn->drag_client->window, dwn->resizing);
XUngrabPointer(dwn->display, CurrentTime);
dwn->drag_client = NULL;
}
api_emit_mouse_button_released(ev->button, ev->x_root, ev->y_root);
}
static void handle_motion_notify(XMotionEvent *ev)
@ -774,6 +783,10 @@ int dwn_init(void)
ai_init();
screenshot_init();
ocr_init();
api_init();
demo_init();
@ -807,6 +820,8 @@ void dwn_cleanup(void)
demo_cleanup();
api_cleanup();
ocr_cleanup();
screenshot_cleanup();
ai_cleanup();
notifications_cleanup();
news_cleanup();
@ -873,6 +888,10 @@ void dwn_run(void)
exa_process_pending();
screenshot_process_pending();
ocr_process_pending();
demo_update();
notifications_update();
@ -966,6 +985,8 @@ int main(int argc, char *argv[])
}
}
XInitThreads();
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
signal(SIGHUP, signal_handler);

View File

@ -7,6 +7,7 @@
#include "notifications.h"
#include "config.h"
#include "util.h"
#include "api.h"
#include <string.h>
#include <stdlib.h>
@ -520,6 +521,8 @@ uint32_t notification_show(const char *app_name, const char *summary,
LOG_DEBUG("Notification %u (%dx%d): %s - %s", notif->id,
notif->width, notif->height, notif->summary, notif->body);
api_emit_notification_shown(notif->id, notif->summary, notif->body);
return notif->id;
}
@ -545,6 +548,8 @@ void notification_close(uint32_t id)
notif->body = NULL;
}
api_emit_notification_closed(id);
dwn_free(notif);
notifications_position();

359
src/ocr.c Normal file
View File

@ -0,0 +1,359 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* OCR text extraction using Tesseract
*/
#include "ocr.h"
#include "util.h"
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include <tesseract/capi.h>
#include <leptonica/allheaders.h>
static TessBaseAPI *tess_api = NULL;
static bool initialized = false;
static pthread_mutex_t ocr_mutex = PTHREAD_MUTEX_INITIALIZER;
static OcrRequest *ocr_queue = NULL;
static atomic_int ocr_shutting_down = 0;
bool ocr_init(void)
{
if (initialized) {
return true;
}
atomic_store(&ocr_shutting_down, 0);
tess_api = TessBaseAPICreate();
if (tess_api == NULL) {
LOG_ERROR("Failed to create Tesseract API");
return false;
}
if (TessBaseAPIInit3(tess_api, NULL, "eng") != 0) {
LOG_WARN("Tesseract initialization failed, OCR unavailable");
TessBaseAPIDelete(tess_api);
tess_api = NULL;
return false;
}
initialized = true;
LOG_INFO("OCR module initialized with Tesseract");
return true;
}
void ocr_cleanup(void)
{
atomic_store(&ocr_shutting_down, 1);
pthread_mutex_lock(&ocr_mutex);
while (ocr_queue != NULL) {
OcrRequest *req = ocr_queue;
ocr_queue = req->next;
pthread_mutex_unlock(&ocr_mutex);
pthread_join(req->thread, NULL);
if (req->png_data != NULL) {
dwn_free(req->png_data);
}
ocr_result_free(&req->result);
dwn_free(req);
pthread_mutex_lock(&ocr_mutex);
}
pthread_mutex_unlock(&ocr_mutex);
if (tess_api != NULL) {
TessBaseAPIEnd(tess_api);
TessBaseAPIDelete(tess_api);
tess_api = NULL;
}
initialized = false;
}
bool ocr_is_available(void)
{
return initialized && tess_api != NULL;
}
const char *ocr_status_string(OcrStatus status)
{
switch (status) {
case OCR_OK:
return "OK";
case OCR_ERROR_INVALID_ARG:
return "Invalid argument";
case OCR_ERROR_INIT:
return "Initialization error";
case OCR_ERROR_IMAGE:
return "Image processing error";
case OCR_ERROR_NO_MEMORY:
return "Out of memory";
case OCR_ERROR_NOT_AVAILABLE:
return "OCR not available";
default:
return "Unknown error";
}
}
void ocr_result_free(OcrResult *result)
{
if (result == NULL) {
return;
}
if (result->text != NULL) {
dwn_free(result->text);
result->text = NULL;
}
result->length = 0;
result->confidence = 0.0f;
}
OcrStatus ocr_extract_from_png(const unsigned char *png_data, size_t size,
const char *language, OcrResult *result)
{
if (result == NULL) {
return OCR_ERROR_INVALID_ARG;
}
memset(result, 0, sizeof(OcrResult));
if (png_data == NULL || size == 0) {
return OCR_ERROR_INVALID_ARG;
}
if (!ocr_is_available()) {
return OCR_ERROR_NOT_AVAILABLE;
}
const char *lang = (language != NULL && language[0] != '\0') ? language : "eng";
if (TessBaseAPIInit3(tess_api, NULL, lang) != 0) {
LOG_WARN("Failed to set language '%s', falling back to 'eng'", lang);
if (TessBaseAPIInit3(tess_api, NULL, "eng") != 0) {
return OCR_ERROR_INIT;
}
}
PIX *image = pixReadMem(png_data, size);
if (image == NULL) {
LOG_ERROR("Failed to read PNG image for OCR");
return OCR_ERROR_IMAGE;
}
TessBaseAPISetImage2(tess_api, image);
char *text = TessBaseAPIGetUTF8Text(tess_api);
if (text == NULL) {
pixDestroy(&image);
return OCR_ERROR_IMAGE;
}
int conf = TessBaseAPIMeanTextConf(tess_api);
float confidence = (float)conf / 100.0f;
size_t text_len = strlen(text);
result->text = dwn_malloc(text_len + 1);
if (result->text == NULL) {
TessDeleteText(text);
pixDestroy(&image);
return OCR_ERROR_NO_MEMORY;
}
memcpy(result->text, text, text_len + 1);
result->length = text_len;
result->confidence = confidence;
TessDeleteText(text);
pixDestroy(&image);
return OCR_OK;
}
static void *ocr_worker(void *arg)
{
OcrRequest *req = (OcrRequest *)arg;
if (atomic_load(&ocr_shutting_down)) {
req->state = OCR_STATE_ERROR;
req->status = OCR_ERROR_NOT_AVAILABLE;
return NULL;
}
TessBaseAPI *local_tess = TessBaseAPICreate();
if (local_tess == NULL) {
req->status = OCR_ERROR_INIT;
req->state = OCR_STATE_ERROR;
return NULL;
}
const char *lang = req->language[0] ? req->language : "eng";
if (TessBaseAPIInit3(local_tess, NULL, lang) != 0) {
if (TessBaseAPIInit3(local_tess, NULL, "eng") != 0) {
TessBaseAPIDelete(local_tess);
req->status = OCR_ERROR_INIT;
req->state = OCR_STATE_ERROR;
return NULL;
}
}
PIX *image = pixReadMem(req->png_data, req->png_size);
if (image == NULL) {
TessBaseAPIEnd(local_tess);
TessBaseAPIDelete(local_tess);
req->status = OCR_ERROR_IMAGE;
req->state = OCR_STATE_ERROR;
return NULL;
}
TessBaseAPISetImage2(local_tess, image);
char *text = TessBaseAPIGetUTF8Text(local_tess);
int conf = TessBaseAPIMeanTextConf(local_tess);
if (text != NULL) {
size_t len = strlen(text);
req->result.text = dwn_malloc(len + 1);
if (req->result.text) {
memcpy(req->result.text, text, len + 1);
req->result.length = len;
req->result.confidence = (float)conf / 100.0f;
req->status = OCR_OK;
req->state = OCR_STATE_COMPLETED;
} else {
req->status = OCR_ERROR_NO_MEMORY;
req->state = OCR_STATE_ERROR;
}
TessDeleteText(text);
} else {
req->status = OCR_ERROR_IMAGE;
req->state = OCR_STATE_ERROR;
}
pixDestroy(&image);
TessBaseAPIEnd(local_tess);
TessBaseAPIDelete(local_tess);
return NULL;
}
OcrRequest *ocr_extract_async(const unsigned char *png_data, size_t size,
const char *language,
void (*callback)(OcrRequest *))
{
if (!initialized || atomic_load(&ocr_shutting_down)) {
return NULL;
}
if (png_data == NULL || size == 0) {
return NULL;
}
OcrRequest *req = dwn_calloc(1, sizeof(OcrRequest));
if (req == NULL) {
return NULL;
}
req->png_data = dwn_malloc(size);
if (req->png_data == NULL) {
dwn_free(req);
return NULL;
}
memcpy(req->png_data, png_data, size);
req->png_size = size;
if (language != NULL && language[0] != '\0') {
strncpy(req->language, language, sizeof(req->language) - 1);
req->language[sizeof(req->language) - 1] = '\0';
}
req->state = OCR_STATE_PENDING;
req->callback = callback;
pthread_mutex_lock(&ocr_mutex);
req->next = ocr_queue;
ocr_queue = req;
pthread_mutex_unlock(&ocr_mutex);
if (pthread_create(&req->thread, NULL, ocr_worker, req) != 0) {
pthread_mutex_lock(&ocr_mutex);
OcrRequest **pp = &ocr_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&ocr_mutex);
dwn_free(req->png_data);
dwn_free(req);
return NULL;
}
LOG_DEBUG("OCR async request started");
return req;
}
void ocr_process_pending(void)
{
if (atomic_load(&ocr_shutting_down)) {
return;
}
pthread_mutex_lock(&ocr_mutex);
OcrRequest **pp = &ocr_queue;
while (*pp != NULL) {
OcrRequest *req = *pp;
if (req->state == OCR_STATE_COMPLETED ||
req->state == OCR_STATE_ERROR) {
pthread_join(req->thread, NULL);
*pp = req->next;
pthread_mutex_unlock(&ocr_mutex);
if (req->callback != NULL) {
req->callback(req);
}
pthread_mutex_lock(&ocr_mutex);
} else {
pp = &(*pp)->next;
}
}
pthread_mutex_unlock(&ocr_mutex);
}
void ocr_cancel(OcrRequest *req)
{
if (req == NULL) {
return;
}
pthread_mutex_lock(&ocr_mutex);
OcrRequest **pp = &ocr_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&ocr_mutex);
pthread_cancel(req->thread);
pthread_join(req->thread, NULL);
if (req->png_data != NULL) {
dwn_free(req->png_data);
}
ocr_result_free(&req->result);
dwn_free(req);
}

628
src/screenshot.c Normal file
View File

@ -0,0 +1,628 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* Screenshot capture implementation using X11 and libpng
*/
#include "screenshot.h"
#include "dwn.h"
#include "client.h"
#include "workspace.h"
#include "util.h"
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include <png.h>
static bool initialized = false;
static pthread_mutex_t screenshot_mutex = PTHREAD_MUTEX_INITIALIZER;
static ScreenshotRequest *screenshot_queue = NULL;
static atomic_int screenshot_shutting_down = 0;
static const char base64_chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
bool screenshot_init(void)
{
atomic_store(&screenshot_shutting_down, 0);
initialized = true;
LOG_INFO("Screenshot module initialized");
return true;
}
void screenshot_cleanup(void)
{
atomic_store(&screenshot_shutting_down, 1);
pthread_mutex_lock(&screenshot_mutex);
while (screenshot_queue != NULL) {
ScreenshotRequest *req = screenshot_queue;
screenshot_queue = req->next;
pthread_mutex_unlock(&screenshot_mutex);
pthread_join(req->thread, NULL);
screenshot_result_free(&req->result);
dwn_free(req);
pthread_mutex_lock(&screenshot_mutex);
}
pthread_mutex_unlock(&screenshot_mutex);
initialized = false;
}
static size_t base64_encoded_size(size_t input_size)
{
return 4 * ((input_size + 2) / 3);
}
char *screenshot_to_base64(const ScreenshotResult *result, size_t *out_len)
{
if (result == NULL || result->data == NULL || result->size == 0) {
return NULL;
}
size_t encoded_size = base64_encoded_size(result->size);
char *encoded = dwn_malloc(encoded_size + 1);
if (encoded == NULL) {
return NULL;
}
const unsigned char *input = result->data;
size_t input_len = result->size;
char *output = encoded;
for (size_t i = 0; i < input_len; i += 3) {
unsigned int n = ((unsigned int)input[i]) << 16;
if (i + 1 < input_len) {
n |= ((unsigned int)input[i + 1]) << 8;
}
if (i + 2 < input_len) {
n |= (unsigned int)input[i + 2];
}
*output++ = base64_chars[(n >> 18) & 0x3F];
*output++ = base64_chars[(n >> 12) & 0x3F];
*output++ = (i + 1 < input_len) ? base64_chars[(n >> 6) & 0x3F] : '=';
*output++ = (i + 2 < input_len) ? base64_chars[n & 0x3F] : '=';
}
*output = '\0';
if (out_len != NULL) {
*out_len = output - encoded;
}
return encoded;
}
void screenshot_result_free(ScreenshotResult *result)
{
if (result == NULL) {
return;
}
if (result->data != NULL) {
dwn_free(result->data);
result->data = NULL;
}
result->size = 0;
result->width = 0;
result->height = 0;
}
const char *screenshot_status_string(ScreenshotStatus status)
{
switch (status) {
case SCREENSHOT_OK:
return "OK";
case SCREENSHOT_ERROR_INVALID_ARG:
return "Invalid argument";
case SCREENSHOT_ERROR_X11:
return "X11 error";
case SCREENSHOT_ERROR_PNG:
return "PNG encoding error";
case SCREENSHOT_ERROR_NO_MEMORY:
return "Out of memory";
case SCREENSHOT_ERROR_WINDOW_NOT_FOUND:
return "Window not found";
default:
return "Unknown error";
}
}
typedef struct {
unsigned char *data;
size_t size;
size_t capacity;
} PngBuffer;
static void png_write_callback(png_structp png, png_bytep data, png_size_t length)
{
PngBuffer *buf = (PngBuffer *)png_get_io_ptr(png);
if (buf == NULL) {
return;
}
size_t new_size = buf->size + length;
if (new_size > buf->capacity) {
size_t new_capacity = buf->capacity * 2;
if (new_capacity < new_size) {
new_capacity = new_size + 1024;
}
unsigned char *new_data = dwn_realloc(buf->data, new_capacity);
if (new_data == NULL) {
png_error(png, "Memory allocation failed");
return;
}
buf->data = new_data;
buf->capacity = new_capacity;
}
memcpy(buf->data + buf->size, data, length);
buf->size = new_size;
}
static void png_flush_callback(png_structp png)
{
(void)png;
}
static ScreenshotStatus ximage_to_png(XImage *img, ScreenshotResult *result)
{
if (img == NULL || result == NULL) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (png == NULL) {
return SCREENSHOT_ERROR_PNG;
}
png_infop info = png_create_info_struct(png);
if (info == NULL) {
png_destroy_write_struct(&png, NULL);
return SCREENSHOT_ERROR_PNG;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
return SCREENSHOT_ERROR_PNG;
}
PngBuffer buf = {0};
buf.capacity = (size_t)(img->width * img->height * 3 / 2 + 1024);
buf.data = dwn_malloc(buf.capacity);
if (buf.data == NULL) {
png_destroy_write_struct(&png, &info);
return SCREENSHOT_ERROR_NO_MEMORY;
}
png_set_write_fn(png, &buf, png_write_callback, png_flush_callback);
png_set_IHDR(png, info, img->width, img->height, 8,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
unsigned char *row = dwn_malloc(img->width * 3);
if (row == NULL) {
dwn_free(buf.data);
png_destroy_write_struct(&png, &info);
return SCREENSHOT_ERROR_NO_MEMORY;
}
unsigned long red_mask = img->red_mask;
unsigned long green_mask = img->green_mask;
unsigned long blue_mask = img->blue_mask;
int red_shift = 0;
int green_shift = 0;
int blue_shift = 0;
while ((red_mask & 1) == 0 && red_shift < 32) {
red_shift++;
red_mask >>= 1;
}
while ((green_mask & 1) == 0 && green_shift < 32) {
green_shift++;
green_mask >>= 1;
}
while ((blue_mask & 1) == 0 && blue_shift < 32) {
blue_shift++;
blue_mask >>= 1;
}
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
unsigned long pixel = XGetPixel(img, x, y);
row[x * 3 + 0] = (pixel >> red_shift) & 0xFF;
row[x * 3 + 1] = (pixel >> green_shift) & 0xFF;
row[x * 3 + 2] = (pixel >> blue_shift) & 0xFF;
}
png_write_row(png, row);
}
png_write_end(png, info);
dwn_free(row);
png_destroy_write_struct(&png, &info);
result->data = buf.data;
result->size = buf.size;
result->width = img->width;
result->height = img->height;
return SCREENSHOT_OK;
}
ScreenshotStatus screenshot_capture_fullscreen(ScreenshotResult *result)
{
if (result == NULL) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
if (dwn == NULL || dwn->display == NULL) {
return SCREENSHOT_ERROR_X11;
}
memset(result, 0, sizeof(ScreenshotResult));
XImage *img = XGetImage(dwn->display, dwn->root,
0, 0, dwn->screen_width, dwn->screen_height,
AllPlanes, ZPixmap);
if (img == NULL) {
LOG_ERROR("XGetImage failed for fullscreen capture");
return SCREENSHOT_ERROR_X11;
}
ScreenshotStatus status = ximage_to_png(img, result);
XDestroyImage(img);
return status;
}
ScreenshotStatus screenshot_capture_window(Window window, ScreenshotResult *result)
{
if (result == NULL) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
if (dwn == NULL || dwn->display == NULL) {
return SCREENSHOT_ERROR_X11;
}
if (window == None) {
return SCREENSHOT_ERROR_WINDOW_NOT_FOUND;
}
memset(result, 0, sizeof(ScreenshotResult));
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, window, &wa)) {
return SCREENSHOT_ERROR_WINDOW_NOT_FOUND;
}
int x, y;
Window child;
XTranslateCoordinates(dwn->display, window, dwn->root, 0, 0, &x, &y, &child);
int width = wa.width;
int height = wa.height;
if (x < 0) {
width += x;
x = 0;
}
if (y < 0) {
height += y;
y = 0;
}
if (x + width > dwn->screen_width) {
width = dwn->screen_width - x;
}
if (y + height > dwn->screen_height) {
height = dwn->screen_height - y;
}
if (width <= 0 || height <= 0) {
return SCREENSHOT_ERROR_X11;
}
XImage *img = XGetImage(dwn->display, dwn->root, x, y, width, height,
AllPlanes, ZPixmap);
if (img == NULL) {
LOG_ERROR("XGetImage failed for window capture");
return SCREENSHOT_ERROR_X11;
}
ScreenshotStatus status = ximage_to_png(img, result);
XDestroyImage(img);
return status;
}
ScreenshotStatus screenshot_capture_active(ScreenshotResult *result)
{
if (result == NULL) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
if (dwn == NULL) {
return SCREENSHOT_ERROR_X11;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return SCREENSHOT_ERROR_WINDOW_NOT_FOUND;
}
Client *c = ws->focused;
Window target = (c->frame != None) ? c->frame : c->window;
return screenshot_capture_window(target, result);
}
ScreenshotStatus screenshot_capture_area(int x, int y, int width, int height, ScreenshotResult *result)
{
if (result == NULL) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
if (width <= 0 || height <= 0) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
if (dwn == NULL || dwn->display == NULL) {
return SCREENSHOT_ERROR_X11;
}
memset(result, 0, sizeof(ScreenshotResult));
if (x < 0) {
width += x;
x = 0;
}
if (y < 0) {
height += y;
y = 0;
}
if (x + width > dwn->screen_width) {
width = dwn->screen_width - x;
}
if (y + height > dwn->screen_height) {
height = dwn->screen_height - y;
}
if (width <= 0 || height <= 0) {
return SCREENSHOT_ERROR_INVALID_ARG;
}
XImage *img = XGetImage(dwn->display, dwn->root, x, y, width, height,
AllPlanes, ZPixmap);
if (img == NULL) {
LOG_ERROR("XGetImage failed for area capture");
return SCREENSHOT_ERROR_X11;
}
ScreenshotStatus status = ximage_to_png(img, result);
XDestroyImage(img);
return status;
}
static void *screenshot_worker(void *arg)
{
ScreenshotRequest *req = (ScreenshotRequest *)arg;
if (atomic_load(&screenshot_shutting_down)) {
req->state = SCREENSHOT_STATE_ERROR;
req->status = SCREENSHOT_ERROR_X11;
return NULL;
}
switch (req->mode) {
case SCREENSHOT_MODE_FULLSCREEN:
req->status = screenshot_capture_fullscreen(&req->result);
break;
case SCREENSHOT_MODE_WINDOW:
req->status = screenshot_capture_window(req->window, &req->result);
break;
case SCREENSHOT_MODE_ACTIVE:
req->status = screenshot_capture_active(&req->result);
break;
case SCREENSHOT_MODE_AREA:
req->status = screenshot_capture_area(req->x, req->y,
req->width, req->height,
&req->result);
break;
}
req->state = (req->status == SCREENSHOT_OK)
? SCREENSHOT_STATE_COMPLETED
: SCREENSHOT_STATE_ERROR;
return NULL;
}
ScreenshotRequest *screenshot_capture_async(ScreenshotMode mode,
void (*callback)(ScreenshotRequest *))
{
if (!initialized || atomic_load(&screenshot_shutting_down)) {
return NULL;
}
ScreenshotRequest *req = dwn_calloc(1, sizeof(ScreenshotRequest));
if (req == NULL) {
return NULL;
}
req->mode = mode;
req->state = SCREENSHOT_STATE_PENDING;
req->callback = callback;
pthread_mutex_lock(&screenshot_mutex);
req->next = screenshot_queue;
screenshot_queue = req;
pthread_mutex_unlock(&screenshot_mutex);
if (pthread_create(&req->thread, NULL, screenshot_worker, req) != 0) {
pthread_mutex_lock(&screenshot_mutex);
ScreenshotRequest **pp = &screenshot_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&screenshot_mutex);
dwn_free(req);
return NULL;
}
LOG_DEBUG("Screenshot async request started (mode=%d)", mode);
return req;
}
ScreenshotRequest *screenshot_capture_window_async(Window window,
void (*callback)(ScreenshotRequest *))
{
if (!initialized || atomic_load(&screenshot_shutting_down)) {
return NULL;
}
ScreenshotRequest *req = dwn_calloc(1, sizeof(ScreenshotRequest));
if (req == NULL) {
return NULL;
}
req->mode = SCREENSHOT_MODE_WINDOW;
req->window = window;
req->state = SCREENSHOT_STATE_PENDING;
req->callback = callback;
pthread_mutex_lock(&screenshot_mutex);
req->next = screenshot_queue;
screenshot_queue = req;
pthread_mutex_unlock(&screenshot_mutex);
if (pthread_create(&req->thread, NULL, screenshot_worker, req) != 0) {
pthread_mutex_lock(&screenshot_mutex);
ScreenshotRequest **pp = &screenshot_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&screenshot_mutex);
dwn_free(req);
return NULL;
}
LOG_DEBUG("Screenshot async window request started");
return req;
}
ScreenshotRequest *screenshot_capture_area_async(int x, int y, int w, int h,
void (*callback)(ScreenshotRequest *))
{
if (!initialized || atomic_load(&screenshot_shutting_down)) {
return NULL;
}
ScreenshotRequest *req = dwn_calloc(1, sizeof(ScreenshotRequest));
if (req == NULL) {
return NULL;
}
req->mode = SCREENSHOT_MODE_AREA;
req->x = x;
req->y = y;
req->width = w;
req->height = h;
req->state = SCREENSHOT_STATE_PENDING;
req->callback = callback;
pthread_mutex_lock(&screenshot_mutex);
req->next = screenshot_queue;
screenshot_queue = req;
pthread_mutex_unlock(&screenshot_mutex);
if (pthread_create(&req->thread, NULL, screenshot_worker, req) != 0) {
pthread_mutex_lock(&screenshot_mutex);
ScreenshotRequest **pp = &screenshot_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&screenshot_mutex);
dwn_free(req);
return NULL;
}
LOG_DEBUG("Screenshot async area request started");
return req;
}
void screenshot_process_pending(void)
{
if (atomic_load(&screenshot_shutting_down)) {
return;
}
pthread_mutex_lock(&screenshot_mutex);
ScreenshotRequest **pp = &screenshot_queue;
while (*pp != NULL) {
ScreenshotRequest *req = *pp;
if (req->state == SCREENSHOT_STATE_COMPLETED ||
req->state == SCREENSHOT_STATE_ERROR) {
pthread_join(req->thread, NULL);
*pp = req->next;
pthread_mutex_unlock(&screenshot_mutex);
if (req->callback != NULL) {
req->callback(req);
}
pthread_mutex_lock(&screenshot_mutex);
} else {
pp = &(*pp)->next;
}
}
pthread_mutex_unlock(&screenshot_mutex);
}
void screenshot_cancel(ScreenshotRequest *req)
{
if (req == NULL) {
return;
}
pthread_mutex_lock(&screenshot_mutex);
ScreenshotRequest **pp = &screenshot_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&screenshot_mutex);
pthread_cancel(req->thread);
pthread_join(req->thread, NULL);
screenshot_result_free(&req->result);
dwn_free(req);
}

View File

@ -11,6 +11,7 @@
#include "util.h"
#include "config.h"
#include "panel.h"
#include "api.h"
#include <string.h>
#include <stdio.h>
@ -114,7 +115,7 @@ void workspace_switch(int index)
XUngrabServer(dwn->display);
XSync(dwn->display, False);
(void)old_workspace;
api_emit_workspace_switched(old_workspace, index);
}
void workspace_switch_next(void)
@ -143,6 +144,7 @@ void workspace_add_client(int workspace, Client *client)
client->workspace = workspace;
api_emit_workspace_window_added(workspace, client->window);
}
void workspace_remove_client(int workspace, Client *client)
@ -159,6 +161,8 @@ void workspace_remove_client(int workspace, Client *client)
if (ws->focused == client) {
ws->focused = NULL;
}
api_emit_workspace_window_removed(workspace, client->window);
}
void workspace_move_client(Client *client, int new_workspace)
@ -179,22 +183,20 @@ void workspace_move_client(Client *client, int new_workspace)
XGrabServer(dwn->display);
workspace_remove_client(old_workspace, client);
workspace_add_client(new_workspace, client);
atoms_set_window_desktop(client->window, new_workspace);
if (new_workspace != dwn->current_workspace) {
client_hide(client);
} else {
client_show(client);
Workspace *target_ws = workspace_get(new_workspace);
if (target_ws != NULL) {
target_ws->focused = client;
}
workspace_arrange(old_workspace);
workspace_arrange(new_workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
workspace_switch(new_workspace);
}
Client *workspace_get_first_client(int workspace)
@ -226,6 +228,7 @@ void workspace_set_layout(int workspace, LayoutType layout)
return;
}
int old_layout = ws->layout;
ws->layout = layout;
XGrabServer(dwn->display);
@ -233,6 +236,8 @@ void workspace_set_layout(int workspace, LayoutType layout)
XUngrabServer(dwn->display);
XSync(dwn->display, False);
api_emit_workspace_layout_changed(workspace, old_layout, layout);
LOG_DEBUG("Workspace %d layout set to %d", workspace + 1, layout);
}
@ -249,6 +254,7 @@ void workspace_cycle_layout(int workspace)
return;
}
int old_layout = ws->layout;
ws->layout = (ws->layout + 1) % LAYOUT_COUNT;
XGrabServer(dwn->display);
@ -256,6 +262,8 @@ void workspace_cycle_layout(int workspace)
XUngrabServer(dwn->display);
XSync(dwn->display, False);
api_emit_workspace_layout_changed(workspace, old_layout, ws->layout);
const char *layout_names[] = { "Tiling", "Floating", "Monocle" };
LOG_INFO("Workspace %d: %s layout", workspace + 1, layout_names[ws->layout]);
}
@ -267,6 +275,8 @@ void workspace_set_master_ratio(int workspace, float ratio)
return;
}
float old_ratio = ws->master_ratio;
if (ratio < 0.1f) ratio = 0.1f;
if (ratio > 0.9f) ratio = 0.9f;
@ -276,6 +286,10 @@ void workspace_set_master_ratio(int workspace, float ratio)
workspace_arrange(workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
if (old_ratio != ws->master_ratio) {
api_emit_workspace_master_ratio_changed(workspace, old_ratio, ws->master_ratio);
}
}
void workspace_adjust_master_ratio(int workspace, float delta)
@ -293,6 +307,8 @@ void workspace_set_master_count(int workspace, int count)
return;
}
int old_count = ws->master_count;
if (count < 1) count = 1;
ws->master_count = count;
@ -300,6 +316,10 @@ void workspace_set_master_count(int workspace, int count)
workspace_arrange(workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
if (old_count != ws->master_count) {
api_emit_workspace_master_count_changed(workspace, old_count, ws->master_count);
}
}
void workspace_adjust_master_count(int workspace, int delta)
@ -323,6 +343,8 @@ void workspace_arrange(int workspace)
}
layout_arrange(workspace);
api_emit_workspace_arranged(workspace, ws->layout);
}
void workspace_arrange_current(void)