511 lines
22 KiB
HTML
511 lines
22 KiB
HTML
|
|
<!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 <retoor@molodetz.nl></p>
|
||
|
|
</footer>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script src="js/main.js"></script>
|
||
|
|
</body>
|
||
|
|
</html>
|