feat: add XDG autostart support for automatic application launching

feat: implement demo mode for automated feature showcase
feat: add XEmbed system tray protocol for external application icons
feat: add window snapping to left and right halves
feat: add minimize/restore toggle with Alt+F9 keybinding
docs: update README with new features and configuration details
build: update build system for new autostart and demo modules
This commit is contained in:
retoor 2025-12-28 09:26:53 +01:00
parent c76a620012
commit d0b1669cb4
53 changed files with 2921 additions and 56 deletions

View File

@ -29,6 +29,7 @@ DWN is a **window manager** - the program that controls how windows look and beh
- 3 window layouts: Tiling (windows don't overlap), Floating (like normal), Monocle (fullscreen) - 3 window layouts: Tiling (windows don't overlap), Floating (like normal), Monocle (fullscreen)
- 9 virtual workspaces to organize your windows - 9 virtual workspaces to organize your windows
- Built-in panels with clock, taskbar, and system tray - Built-in panels with clock, taskbar, and system tray
- XDG Autostart support (automatically starts nm-applet, blueman, etc.)
- WiFi network selector with dropdown menu (like XFCE) - WiFi network selector with dropdown menu (like XFCE)
- Audio volume indicator with scroll-to-adjust - Audio volume indicator with scroll-to-adjust
- Desktop notifications - Desktop notifications
@ -102,6 +103,7 @@ sudo make install
| `Super + Backspace` | **Exit DWN** (important!) | | `Super + Backspace` | **Exit DWN** (important!) |
| `Super + S` | **Show all shortcuts** | | `Super + S` | **Show all shortcuts** |
| `Super + T` | **Start interactive tutorial** | | `Super + T` | **Start interactive tutorial** |
| `Super + Shift + D` | **Demo mode** (automated feature showcase) |
### Workspaces ### Workspaces
@ -116,9 +118,10 @@ sudo make install
| Keys | What it does | | Keys | What it does |
|------|--------------| |------|--------------|
| `Super + F9` | Toggle floating mode | | `Alt + F9` | Toggle minimize/restore |
| `Alt + F10` | Toggle maximize | | `Alt + F10` | Toggle maximize |
| `Alt + F11` | Toggle fullscreen | | `Alt + F11` | Toggle fullscreen |
| `Super + F9` | Toggle floating mode |
| `Alt + Tab` | Cycle windows forward | | `Alt + Tab` | Cycle windows forward |
| `Alt + Shift + Tab` | Cycle windows backward | | `Alt + Shift + Tab` | Cycle windows backward |
@ -131,6 +134,8 @@ sudo make install
| `Super + L` | Expand main area | | `Super + L` | Expand main area |
| `Super + I` | Add window to main area | | `Super + I` | Add window to main area |
| `Super + D` | Remove window from main area | | `Super + D` | Remove window from main area |
| `Super + Left` | Snap window to left half (50% width) |
| `Super + Right` | Snap window to right half (50% width) |
### AI Features (Optional) ### AI Features (Optional)
@ -160,7 +165,22 @@ The bottom panel displays a scrolling news ticker. Navigate articles with these
## System Tray ## System Tray
The top panel includes a system tray on the right side with battery, audio, and WiFi indicators. The top panel includes a system tray on the right side with support for external application icons plus built-in battery, audio, and WiFi indicators.
### XEmbed System Tray (External Application Icons)
DWN implements the freedesktop.org XEmbed System Tray protocol, allowing external applications to dock their status icons in the panel - just like XFCE, GNOME, or KDE. This means applications like:
- **Telegram** - Shows notification icon when messages arrive
- **nm-applet** - NetworkManager GUI applet
- **blueman-applet** - Bluetooth manager
- **pasystray** - PulseAudio volume control
- **udiskie** - USB device automounter
- **clipit/parcellite** - Clipboard managers
...will automatically appear in your system tray when launched.
Simply start any application with tray icon support and it will dock itself in the panel. Click on icons to interact with them - clicks are forwarded to the application.
### Battery Indicator ### Battery Indicator
@ -243,6 +263,25 @@ gap = 10
border_width = 2 border_width = 2
``` ```
**Configure autostart:**
```ini
[autostart]
enabled = true
xdg_autostart = true
path = ~/.config/dwn/autostart.d
```
DWN automatically starts applications from XDG autostart directories:
- `/etc/xdg/autostart/` - System apps (nm-applet, blueman, etc.)
- `~/.config/autostart/` - User apps
- `~/.config/dwn/autostart.d/` - DWN-specific symlinks
To add custom autostart apps:
```bash
mkdir -p ~/.config/dwn/autostart.d
ln -s /usr/bin/telegram-desktop ~/.config/dwn/autostart.d/
```
--- ---
## AI Features (Optional) ## AI Features (Optional)

BIN
bin/dwn

Binary file not shown.

Binary file not shown.

Binary file not shown.

7
build/autostart.d Normal file
View File

@ -0,0 +1,7 @@
build/autostart.o: src/autostart.c include/autostart.h include/dwn.h \
include/config.h include/dwn.h include/util.h
include/autostart.h:
include/dwn.h:
include/config.h:
include/dwn.h:
include/util.h:

BIN
build/autostart.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

52
build/demo.d Normal file
View File

@ -0,0 +1,52 @@
build/demo.o: src/demo.c include/demo.h include/dwn.h \
include/notifications.h include/dwn.h /usr/include/dbus-1.0/dbus/dbus.h \
/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \
/usr/include/dbus-1.0/dbus/dbus-macros.h \
/usr/include/dbus-1.0/dbus/dbus-address.h \
/usr/include/dbus-1.0/dbus/dbus-types.h \
/usr/include/dbus-1.0/dbus/dbus-errors.h \
/usr/include/dbus-1.0/dbus/dbus-protocol.h \
/usr/include/dbus-1.0/dbus/dbus-bus.h \
/usr/include/dbus-1.0/dbus/dbus-connection.h \
/usr/include/dbus-1.0/dbus/dbus-memory.h \
/usr/include/dbus-1.0/dbus/dbus-message.h \
/usr/include/dbus-1.0/dbus/dbus-shared.h \
/usr/include/dbus-1.0/dbus/dbus-misc.h \
/usr/include/dbus-1.0/dbus/dbus-pending-call.h \
/usr/include/dbus-1.0/dbus/dbus-server.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/workspace.h \
include/client.h include/layout.h include/keys.h include/ai.h \
include/news.h include/util.h include/panel.h include/config.h
include/demo.h:
include/dwn.h:
include/notifications.h:
include/dwn.h:
/usr/include/dbus-1.0/dbus/dbus.h:
/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h:
/usr/include/dbus-1.0/dbus/dbus-macros.h:
/usr/include/dbus-1.0/dbus/dbus-address.h:
/usr/include/dbus-1.0/dbus/dbus-types.h:
/usr/include/dbus-1.0/dbus/dbus-errors.h:
/usr/include/dbus-1.0/dbus/dbus-protocol.h:
/usr/include/dbus-1.0/dbus/dbus-bus.h:
/usr/include/dbus-1.0/dbus/dbus-connection.h:
/usr/include/dbus-1.0/dbus/dbus-memory.h:
/usr/include/dbus-1.0/dbus/dbus-message.h:
/usr/include/dbus-1.0/dbus/dbus-shared.h:
/usr/include/dbus-1.0/dbus/dbus-misc.h:
/usr/include/dbus-1.0/dbus/dbus-pending-call.h:
/usr/include/dbus-1.0/dbus/dbus-server.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/workspace.h:
include/client.h:
include/layout.h:
include/keys.h:
include/ai.h:
include/news.h:
include/util.h:
include/panel.h:
include/config.h:

BIN
build/demo.o Normal file

Binary file not shown.

View File

@ -18,7 +18,7 @@ build/keys.o: src/keys.c include/keys.h include/dwn.h include/client.h \
/usr/include/dbus-1.0/dbus/dbus-signature.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-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/news.h \ /usr/include/dbus-1.0/dbus/dbus-threads.h include/news.h \
include/applauncher.h include/applauncher.h include/decorations.h include/demo.h
include/keys.h: include/keys.h:
include/dwn.h: include/dwn.h:
include/client.h: include/client.h:
@ -47,3 +47,5 @@ include/notifications.h:
/usr/include/dbus-1.0/dbus/dbus-threads.h: /usr/include/dbus-1.0/dbus/dbus-threads.h:
include/news.h: include/news.h:
include/applauncher.h: include/applauncher.h:
include/decorations.h:
include/demo.h:

Binary file not shown.

View File

@ -19,7 +19,8 @@ build/main.o: src/main.c include/dwn.h include/config.h include/dwn.h \
/usr/include/dbus-1.0/dbus/dbus-signature.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-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/systray.h \ /usr/include/dbus-1.0/dbus/dbus-threads.h include/systray.h \
include/news.h include/applauncher.h include/ai.h include/util.h include/news.h include/applauncher.h include/ai.h include/autostart.h \
include/demo.h include/util.h
include/dwn.h: include/dwn.h:
include/config.h: include/config.h:
include/dwn.h: include/dwn.h:
@ -53,4 +54,6 @@ include/systray.h:
include/news.h: include/news.h:
include/applauncher.h: include/applauncher.h:
include/ai.h: include/ai.h:
include/autostart.h:
include/demo.h:
include/util.h: include/util.h:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -17,7 +17,7 @@ build/systray.o: src/systray.c include/systray.h include/dwn.h \
/usr/include/dbus-1.0/dbus/dbus-server.h \ /usr/include/dbus-1.0/dbus/dbus-server.h \
/usr/include/dbus-1.0/dbus/dbus-signature.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-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h /usr/include/dbus-1.0/dbus/dbus-threads.h include/atoms.h
include/systray.h: include/systray.h:
include/dwn.h: include/dwn.h:
include/panel.h: include/panel.h:
@ -42,3 +42,4 @@ include/notifications.h:
/usr/include/dbus-1.0/dbus/dbus-signature.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-syntax.h:
/usr/include/dbus-1.0/dbus/dbus-threads.h: /usr/include/dbus-1.0/dbus/dbus-threads.h:
include/atoms.h:

Binary file not shown.

View File

@ -1,6 +1,6 @@
build/workspace.o: src/workspace.c include/workspace.h include/dwn.h \ 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/client.h include/layout.h include/atoms.h include/util.h \
include/config.h include/config.h include/panel.h
include/workspace.h: include/workspace.h:
include/dwn.h: include/dwn.h:
include/client.h: include/client.h:
@ -8,3 +8,4 @@ include/layout.h:
include/atoms.h: include/atoms.h:
include/util.h: include/util.h:
include/config.h: include/config.h:
include/panel.h:

Binary file not shown.

View File

@ -91,3 +91,16 @@ model = google/gemini-2.0-flash-exp:free
# Can also be set via EXA_API_KEY environment variable # Can also be set via EXA_API_KEY environment variable
# Sign up and get your key at: https://dashboard.exa.ai/api-keys # Sign up and get your key at: https://dashboard.exa.ai/api-keys
# exa_api_key = your-exa-key-here # exa_api_key = your-exa-key-here
[autostart]
# Enable XDG autostart support (default: true)
# Automatically starts applications from XDG autostart directories
enabled = true
# Scan XDG .desktop files (default: true)
# Directories: /etc/xdg/autostart and ~/.config/autostart
xdg_autostart = true
# Additional directory for symlinks and scripts (default: ~/.config/dwn/autostart.d)
# Create symlinks to binaries you want to start: ln -s /usr/bin/telegram-desktop ~/.config/dwn/autostart.d/
path = ~/.config/dwn/autostart.d

View File

@ -76,6 +76,8 @@ typedef struct {
Atom NET_SYSTEM_TRAY_OPCODE; Atom NET_SYSTEM_TRAY_OPCODE;
Atom NET_SYSTEM_TRAY_S0; Atom NET_SYSTEM_TRAY_S0;
Atom NET_SYSTEM_TRAY_ORIENTATION;
Atom NET_SYSTEM_TRAY_VISUAL;
Atom MANAGER; Atom MANAGER;
Atom XEMBED; Atom XEMBED;
Atom XEMBED_INFO; Atom XEMBED_INFO;
@ -130,4 +132,6 @@ void atoms_send_protocol(Window window, Atom protocol, Time timestamp);
void atoms_send_client_message(Window window, Atom message_type, void atoms_send_client_message(Window window, Atom message_type,
long data0, long data1, long data2, long data3, long data4); long data0, long data1, long data2, long data3, long data4);
bool atoms_update_wm_state(Window window, Atom state, bool add);
#endif #endif

14
include/autostart.h Normal file
View File

@ -0,0 +1,14 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* XDG Autostart support
*/
#ifndef DWN_AUTOSTART_H
#define DWN_AUTOSTART_H
void autostart_init(void);
void autostart_run(void);
void autostart_cleanup(void);
#endif

View File

@ -54,6 +54,10 @@ struct Config {
char config_path[512]; char config_path[512];
char log_path[512]; char log_path[512];
bool autostart_enabled;
bool autostart_xdg;
char autostart_path[512];
}; };
Config *config_create(void); Config *config_create(void);

36
include/demo.h Normal file
View File

@ -0,0 +1,36 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* Demo mode - automated feature showcase
*/
#ifndef DWN_DEMO_H
#define DWN_DEMO_H
#include <stdbool.h>
typedef enum {
DEMO_IDLE,
DEMO_INTRO,
DEMO_WINDOW_MGMT,
DEMO_WORKSPACES,
DEMO_LAYOUTS,
DEMO_SNAPPING,
DEMO_PANELS,
DEMO_AI,
DEMO_NEWS,
DEMO_SHORTCUTS,
DEMO_COMPLETE
} DemoPhase;
void demo_init(void);
void demo_cleanup(void);
void demo_start(void);
void demo_stop(void);
void demo_update(void);
bool demo_is_active(void);
DemoPhase demo_get_phase(void);
int demo_get_step(void);
const char *demo_get_phase_name(DemoPhase phase);
#endif

View File

@ -50,6 +50,7 @@ void key_cycle_layout(void);
void key_toggle_floating(void); void key_toggle_floating(void);
void key_toggle_fullscreen(void); void key_toggle_fullscreen(void);
void key_toggle_maximize(void); void key_toggle_maximize(void);
void key_toggle_minimize(void);
void key_focus_next(void); void key_focus_next(void);
void key_focus_prev(void); void key_focus_prev(void);
void key_workspace_next(void); void key_workspace_next(void);
@ -80,6 +81,9 @@ void key_toggle_ai(void);
void key_ai_command(void); void key_ai_command(void);
void key_show_shortcuts(void); void key_show_shortcuts(void);
void key_start_tutorial(void); void key_start_tutorial(void);
void key_snap_left(void);
void key_snap_right(void);
void key_start_demo(void);
void tutorial_start(void); void tutorial_start(void);
void tutorial_stop(void); void tutorial_stop(void);

View File

@ -45,6 +45,7 @@ Client *panel_hit_test_taskbar(Panel *panel, int x, int y);
void panel_show(Panel *panel); void panel_show(Panel *panel);
void panel_hide(Panel *panel); void panel_hide(Panel *panel);
void panel_toggle(Panel *panel); void panel_toggle(Panel *panel);
void panel_raise_all(void);
void panel_update_clock(void); void panel_update_clock(void);

View File

@ -1,7 +1,7 @@
/* /*
* DWN - Desktop Window Manager * DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl> * retoor <retoor@molodetz.nl>
* System tray widgets (WiFi, Audio, etc.) * System tray widgets (WiFi, Audio, etc.) and XEmbed protocol
*/ */
#ifndef DWN_SYSTRAY_H #ifndef DWN_SYSTRAY_H
@ -11,6 +11,35 @@
#include <stdbool.h> #include <stdbool.h>
#define MAX_WIFI_NETWORKS 20 #define MAX_WIFI_NETWORKS 20
#define MAX_TRAY_ICONS 32
#define TRAY_ICON_SIZE 22
#define TRAY_ICON_SPACING 4
#define SYSTEM_TRAY_REQUEST_DOCK 0
#define SYSTEM_TRAY_BEGIN_MESSAGE 1
#define SYSTEM_TRAY_CANCEL_MESSAGE 2
#define XEMBED_EMBEDDED_NOTIFY 0
#define XEMBED_WINDOW_ACTIVATE 1
#define XEMBED_WINDOW_DEACTIVATE 2
#define XEMBED_REQUEST_FOCUS 3
#define XEMBED_FOCUS_IN 4
#define XEMBED_FOCUS_OUT 5
#define XEMBED_MAPPED (1 << 0)
#define XEMBED_PROTOCOL_VERSION 0
typedef struct {
Window window;
int x;
int width;
int height;
bool mapped;
} TrayIcon;
extern TrayIcon tray_icons[MAX_TRAY_ICONS];
extern int tray_icon_count;
extern Window tray_selection_owner;
typedef struct { typedef struct {
char ssid[64]; char ssid[64];
@ -114,4 +143,16 @@ void systray_unlock(void);
BatteryState systray_get_battery_snapshot(void); BatteryState systray_get_battery_snapshot(void);
AudioState systray_get_audio_snapshot(void); AudioState systray_get_audio_snapshot(void);
void xembed_init(void);
void xembed_cleanup(void);
bool xembed_dock_icon(Window icon_win);
void xembed_remove_icon(Window icon_win);
TrayIcon *xembed_find_icon(Window icon_win);
void xembed_send_message(Window icon, long message, long detail, long data1, long data2);
int xembed_get_icons_width(void);
void xembed_render_icons(Panel *panel, int x);
void xembed_handle_click(int x, int y, int button);
int xembed_hit_test(int x);
void xembed_update_icon_state(Window icon_win);
#endif #endif

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -389,6 +390,7 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -62,7 +63,7 @@
<div style="color: var(--text-muted);">Lines of Code</div> <div style="color: var(--text-muted);">Lines of Code</div>
</div> </div>
<div> <div>
<div style="font-size: 2rem; font-weight: 700; color: var(--primary);">12</div> <div style="font-size: 2rem; font-weight: 700; color: var(--primary);">13</div>
<div style="color: var(--text-muted);">Core Modules</div> <div style="color: var(--text-muted);">Core Modules</div>
</div> </div>
<div> <div>
@ -170,6 +171,11 @@
<td><code>ai.c</code></td> <td><code>ai.c</code></td>
<td>Async OpenRouter API integration, Exa semantic search</td> <td>Async OpenRouter API integration, Exa semantic search</td>
</tr> </tr>
<tr>
<td><strong>Autostart</strong></td>
<td><code>autostart.c</code></td>
<td>XDG Autostart support, .desktop file parsing, concurrent app launch</td>
</tr>
<tr> <tr>
<td><strong>Util</strong></td> <td><strong>Util</strong></td>
<td><code>util.c</code></td> <td><code>util.c</code></td>
@ -196,6 +202,8 @@
│ └── config.c │ └── config.c
├── notifications.c (independent) ├── notifications.c (independent)
├── ai.c (independent) ├── ai.c (independent)
├── autostart.c
│ └── config.c
└── keys.c └── keys.c
└── config.c</code></pre> └── config.c</code></pre>
</div> </div>
@ -490,6 +498,7 @@ extern DWNState *dwn; // Global singleton</code></pre>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -387,6 +388,50 @@ exa_api_key = your-exa-key-here</code></pre>
<p style="margin: 0;">For better security, use environment variables instead of storing API keys in the config file: <p style="margin: 0;">For better security, use environment variables instead of storing API keys in the config file:
<code>export OPENROUTER_API_KEY=sk-or-v1-...</code></p> <code>export OPENROUTER_API_KEY=sk-or-v1-...</code></p>
</div> </div>
<h2 id="autostart" style="margin-top: 3rem;">[autostart] - XDG Autostart</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Configure automatic application startup. DWN follows the XDG Autostart specification.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>enabled</code></td>
<td>Enable/disable all autostart functionality (default: <code>true</code>)</td>
</tr>
<tr>
<td><code>xdg_autostart</code></td>
<td>Scan XDG .desktop files from /etc/xdg/autostart and ~/.config/autostart (default: <code>true</code>)</td>
</tr>
<tr>
<td><code>path</code></td>
<td>Additional directory for symlinks/scripts (default: <code>~/.config/dwn/autostart.d</code>)</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[autostart]
enabled = true
xdg_autostart = true
path = ~/.config/dwn/autostart.d</code></pre>
<p style="color: var(--text-muted); margin-top: 1rem;">
<strong>Directories scanned:</strong>
</p>
<ul style="color: var(--text-muted);">
<li><code>/etc/xdg/autostart/</code> - System defaults (nm-applet, blueman, power-manager)</li>
<li><code>~/.config/autostart/</code> - User XDG autostart entries</li>
<li><code>~/.config/dwn/autostart.d/</code> - DWN-specific symlinks and scripts</li>
</ul>
<h2 style="margin-top: 3rem;">Complete Configuration Example</h2> <h2 style="margin-top: 3rem;">Complete Configuration Example</h2>
<div class="code-header"> <div class="code-header">
<span>~/.config/dwn/config</span> <span>~/.config/dwn/config</span>
@ -429,7 +474,11 @@ notification_bg = #2a2a3e
notification_fg = #ffffff notification_fg = #ffffff
[ai] [ai]
model = google/gemini-2.0-flash-exp:free model = google/gemini-2.0-flash-exp:free
# API keys via environment variables recommended</code></pre> # API keys via environment variables recommended
[autostart]
enabled = true
xdg_autostart = true
path = ~/.config/dwn/autostart.d</code></pre>
</div> </div>
</section> </section>
</main> </main>
@ -450,6 +499,7 @@ model = google/gemini-2.0-flash-exp:free
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

779
site/design-patterns.html Normal file
View File

@ -0,0 +1,779 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="DWN Design Patterns - Comprehensive documentation of design patterns, architectural decisions, and research sources used in the DWN window manager.">
<meta name="keywords" content="design patterns, C programming, opaque pointer, vtable, factory pattern, observer pattern, EWMH, ICCCM, XEmbed">
<title>Design Patterns - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
<a href="design-patterns.html" class="active">Design Patterns</a>
</div>
</li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero hero-small">
<div class="container">
<h1>Design Patterns & Architecture</h1>
<p class="subtitle">
Comprehensive documentation of the design patterns, architectural decisions,
and research that shaped DWN's implementation.
</p>
</div>
</section>
<section class="section">
<div class="container docs-container">
<aside class="docs-sidebar">
<h3>Contents</h3>
<ul>
<li><a href="#overview">Overview</a></li>
<li><a href="#c-patterns">C Design Patterns</a></li>
<li><a href="#opaque-pointer">Opaque Pointer</a></li>
<li><a href="#goto-cleanup">Goto Cleanup</a></li>
<li><a href="#vtable">Vtable Polymorphism</a></li>
<li><a href="#factory">Factory Pattern</a></li>
<li><a href="#observer">Observer Pattern</a></li>
<li><a href="#singleton">Singleton Pattern</a></li>
<li><a href="#double-fork">Double Fork Daemon</a></li>
<li><a href="#x11-protocols">X11 Protocols</a></li>
<li><a href="#ewmh">EWMH Specification</a></li>
<li><a href="#icccm">ICCCM Standard</a></li>
<li><a href="#xembed">XEmbed Protocol</a></li>
<li><a href="#systray">System Tray Protocol</a></li>
<li><a href="#xdg">XDG Specifications</a></li>
<li><a href="#async">Async Patterns</a></li>
<li><a href="#modular">Modular Architecture</a></li>
<li><a href="#defensive">Defensive Programming</a></li>
<li><a href="#sources">Research Sources</a></li>
</ul>
</aside>
<div class="docs-content">
<h2 id="overview">Overview</h2>
<p>
DWN is built using professional C design patterns that provide encapsulation,
modularity, and maintainability without the overhead of C++. This document
details the patterns used, the rationale behind architectural decisions,
and links to authoritative sources.
</p>
<div class="alert alert-info">
<strong class="alert-title">Design Philosophy</strong>
<p style="margin: 0;">
DWN prioritizes simplicity, readability, and defensive programming.
Each module has a single responsibility with well-defined interfaces.
The codebase follows the principle: "Simple is better than complex."
</p>
</div>
<h2 id="c-patterns" style="margin-top: 3rem;">C Design Patterns</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
While C lacks native object-oriented features, these patterns provide
equivalent functionality with minimal overhead.
</p>
<h3 id="opaque-pointer">Opaque Pointer Pattern</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Hide implementation details from API consumers, enabling changes without recompilation.</p>
<h4>How It Works</h4>
<p>The header declares a pointer to an incomplete type. The struct definition
exists only in the implementation file, preventing direct member access.</p>
<div class="code-header">
<span>Header (public)</span>
</div>
<pre><code>typedef struct config_t *Config;
Config config_create(void);
void config_destroy(Config cfg);</code></pre>
<div class="code-header">
<span>Implementation (private)</span>
</div>
<pre><code>struct config_t {
int border_width;
char terminal[128];
};</code></pre>
<h4>Benefits</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>API consumers cannot access internal fields directly</li>
<li>Implementation can change without breaking client code</li>
<li>Enforces encapsulation at compile time</li>
</ul>
<h4>Used In</h4>
<p style="color: var(--text-muted);">
<code>config.c</code> - Configuration management
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://interrupt.memfault.com/blog/opaque-pointers" target="_blank">Practical Design Patterns: Opaque Pointers and Objects in C</a> - Memfault</li>
<li><a href="https://en.wikipedia.org/wiki/Opaque_pointer" target="_blank">Opaque Pointer</a> - Wikipedia</li>
<li><a href="https://wiki.sei.cmu.edu/confluence/display/c/DCL12-C.+Implement+abstract+data+types+using+opaque+types" target="_blank">DCL12-C: Implement abstract data types using opaque types</a> - SEI CERT C Coding Standard</li>
<li><a href="https://blog.mbedded.ninja/programming/design-patterns/opaque-pointers/" target="_blank">Opaque Pointers</a> - mbedded.ninja</li>
</ul>
</div>
<h3 id="goto-cleanup">Goto Cleanup Pattern</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Centralize resource cleanup in functions that acquire multiple resources,
preventing memory leaks and ensuring proper deallocation on all code paths.</p>
<h4>How It Works</h4>
<p>Resources are initialized to safe values (NULL). On error, execution jumps
to a cleanup label. The cleanup section safely releases all resources.</p>
<div class="code-header">
<span>Example</span>
</div>
<pre><code>int process_file(const char *path) {
char *buf = NULL;
FILE *f = NULL;
int status = -1;
buf = malloc(1024);
if (!buf) goto cleanup;
f = fopen(path, "r");
if (!f) goto cleanup;
// ... processing ...
status = 0;
cleanup:
free(buf); // safe: free(NULL) is no-op
if (f) fclose(f);
return status;
}</code></pre>
<h4>Benefits</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>Single cleanup point prevents code duplication</li>
<li>All error paths properly release resources</li>
<li>Used extensively in Linux kernel and SQLite</li>
</ul>
<h4>Used In</h4>
<p style="color: var(--text-muted);">
<code>config.c</code>, <code>news.c</code>, <code>ai.c</code> - File and network operations
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://wiki.sei.cmu.edu/confluence/display/c/MEM12-C.+Consider+using+a+goto+chain+when+leaving+a+function+on+error+when+using+and+releasing+resources" target="_blank">MEM12-C: Using goto chain for error handling</a> - SEI CERT C Coding Standard</li>
<li><a href="https://eli.thegreenplace.net/2009/04/27/using-goto-for-error-handling-in-c" target="_blank">Using goto for error handling in C</a> - Eli Bendersky</li>
<li><a href="https://www.geeksforgeeks.org/c/using-goto-for-exception-handling-in-c/" target="_blank">Using goto for Exception Handling in C</a> - GeeksforGeeks</li>
<li><a href="https://ayende.com/blog/183521-C/error-handling-via-goto-in-c" target="_blank">Error handling via GOTO in C</a> - Ayende Rahien</li>
</ul>
</div>
<h3 id="vtable">Vtable Polymorphism</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Enable runtime polymorphism in C using function pointer tables,
allowing different implementations to share a common interface.</p>
<h4>How It Works</h4>
<p>A struct of function pointers (vtable) defines the interface. Objects contain
a pointer to their vtable. Calling through the vtable invokes the correct implementation.</p>
<div class="code-header">
<span>Example</span>
</div>
<pre><code>typedef struct Widget Widget;
typedef struct {
void (*draw)(Widget *self);
void (*destroy)(Widget *self);
} WidgetVtable;
struct Widget {
const WidgetVtable *vtable;
int x, y, width, height;
};
// Usage: widget->vtable->draw(widget);</code></pre>
<h4>Benefits</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>Runtime dispatch without language support</li>
<li>New implementations added without modifying existing code</li>
<li>Same pattern used by C++ compilers internally</li>
</ul>
<h4>Used In</h4>
<p style="color: var(--text-muted);">
<code>panel.c</code> - Widget rendering system
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://en.wikipedia.org/wiki/Virtual_method_table" target="_blank">Virtual Method Table</a> - Wikipedia</li>
<li><a href="https://embeddedartistry.com/fieldatlas/technique-inheritance-and-polymorphism-in-c/" target="_blank">Inheritance and Polymorphism in C</a> - Embedded Artistry</li>
<li><a href="https://www.state-machine.com/doc/AN_Simple_OOP_in_C.pdf" target="_blank">Object-Oriented Programming in C</a> - Quantum Leaps (PDF)</li>
<li><a href="https://www.embedded.com/programming-embedded-systems-polymorphism-in-c-2/" target="_blank">Programming embedded systems: polymorphism in C</a> - Embedded.com</li>
</ul>
</div>
<h3 id="factory">Factory Pattern</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Encapsulate object creation logic, allowing the system to create objects
without specifying their exact types.</p>
<h4>How It Works</h4>
<p>A factory function takes parameters describing what to create and returns
a pointer to the appropriate object type.</p>
<div class="code-header">
<span>Example</span>
</div>
<pre><code>typedef enum { WIDGET_BUTTON, WIDGET_LABEL } WidgetType;
Widget *widget_create(WidgetType type) {
switch (type) {
case WIDGET_BUTTON: return button_create();
case WIDGET_LABEL: return label_create();
default: return NULL;
}
}</code></pre>
<h4>Benefits</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>Centralizes object creation logic</li>
<li>New types added without changing client code</li>
<li>Supports Open/Closed Principle</li>
</ul>
<h4>Used In</h4>
<p style="color: var(--text-muted);">
<code>config.c</code>, <code>client.c</code>, <code>workspace.c</code> - Object lifecycle management
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://refactoring.guru/design-patterns/factory-method" target="_blank">Factory Method Pattern</a> - Refactoring Guru</li>
<li><a href="https://en.wikipedia.org/wiki/Factory_method_pattern" target="_blank">Factory Method Pattern</a> - Wikipedia</li>
<li><a href="https://sourcemaking.com/design_patterns" target="_blank">Design Patterns</a> - SourceMaking</li>
</ul>
</div>
<h3 id="observer">Observer Pattern</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Implement event-driven communication where subjects notify observers
of state changes without tight coupling.</p>
<h4>How It Works</h4>
<p>Observers register callback functions with subjects. When events occur,
the subject iterates through registered callbacks and invokes them.</p>
<div class="code-header">
<span>Example</span>
</div>
<pre><code>typedef void (*EventCallback)(void *data);
void events_register(EventType type, EventCallback cb, void *data);
void events_emit(EventType type);
// Notification triggers all registered callbacks</code></pre>
<h4>Benefits</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>Loose coupling between event sources and handlers</li>
<li>Observers added/removed without modifying subjects</li>
<li>Foundation of event-driven programming</li>
</ul>
<h4>Used In</h4>
<p style="color: var(--text-muted);">
<code>keys.c</code> - Keyboard event handling, <code>notifications.c</code> - D-Bus signals
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://refactoring.guru/design-patterns/observer" target="_blank">Observer Pattern</a> - Refactoring Guru</li>
<li><a href="https://en.wikipedia.org/wiki/Observer_pattern" target="_blank">Observer Pattern</a> - Wikipedia</li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern" target="_blank">Observer Design Pattern</a> - Microsoft Learn</li>
</ul>
</div>
<h3 id="singleton">Singleton / Global State</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Provide a single, globally accessible instance of the window manager state,
simplifying module communication.</p>
<h4>Implementation in DWN</h4>
<p>DWN uses a single <code>DWNState</code> structure accessible via the global
<code>dwn</code> pointer. This is appropriate for a window manager where
exactly one instance exists per X11 session.</p>
<div class="code-header">
<span>Example</span>
</div>
<pre><code>// Global state pointer
extern DWNState *dwn;
// Access from any module
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->terminal;
}</code></pre>
<h4>Rationale</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>Window manager is inherently a singleton per X session</li>
<li>Simplifies inter-module communication</li>
<li>All state centralized for easier debugging</li>
</ul>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://refactoring.guru/design-patterns/singleton" target="_blank">Singleton Pattern</a> - Refactoring Guru</li>
<li><a href="https://gameprogrammingpatterns.com/singleton.html" target="_blank">Singleton</a> - Game Programming Patterns</li>
<li><a href="https://en.wikipedia.org/wiki/Singleton_pattern" target="_blank">Singleton Pattern</a> - Wikipedia</li>
</ul>
</div>
<h3 id="double-fork">Double Fork Daemon Pattern</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Launch background processes that are fully detached from the parent,
preventing zombie processes and terminal reattachment.</p>
<h4>How It Works</h4>
<p>The pattern uses two fork() calls: the first creates a child that calls
setsid() to become a session leader, then forks again. The grandchild
cannot reacquire a controlling terminal.</p>
<div class="code-header">
<span>Implementation in DWN</span>
</div>
<pre><code>int spawn_async(const char *cmd) {
pid_t pid = fork();
if (pid == 0) {
setsid(); // New session
pid_t pid2 = fork(); // Second fork
if (pid2 == 0) {
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS); // Intermediate exits
}
waitpid(pid, &status, 0); // Only wait for intermediate
return 0;
}</code></pre>
<h4>Benefits</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>Process fully detached from parent</li>
<li>No zombie processes (intermediate is reaped immediately)</li>
<li>Cannot reacquire controlling terminal</li>
<li>Non-blocking for the caller</li>
</ul>
<h4>Used In</h4>
<p style="color: var(--text-muted);">
<code>util.c</code> - spawn_async(), <code>autostart.c</code>, <code>applauncher.c</code>
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://0xjet.github.io/3OHA/2022/04/11/post.html" target="_blank">UNIX daemonization and the double fork</a> - Juan Tapiador</li>
<li><a href="https://www.digitalbunker.dev/understanding-daemons-unix/" target="_blank">Understanding Daemons</a> - Digital Bunker</li>
<li><a href="https://lloydrochester.com/post/c/unix-daemon-example/" target="_blank">Daemon Example in C</a> - Lloyd Rochester</li>
<li><a href="https://goral.net.pl/post/double-fork/" target="_blank">Double Fork</a> - Michal Goral</li>
</ul>
</div>
<h2 id="x11-protocols" style="margin-top: 3rem;">X11 Window Manager Protocols</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
DWN implements several freedesktop.org specifications for cross-desktop compatibility.
</p>
<h3 id="ewmh">Extended Window Manager Hints (EWMH)</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Define interactions between window managers, compositing managers, applications,
and desktop utilities in a standardized way.</p>
<h4>Key Features Implemented</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>_NET_SUPPORTED</code> - List of supported hints</li>
<li><code>_NET_CLIENT_LIST</code> - List of managed windows</li>
<li><code>_NET_CURRENT_DESKTOP</code> - Active workspace</li>
<li><code>_NET_WM_STATE</code> - Window states (fullscreen, maximized)</li>
<li><code>_NET_ACTIVE_WINDOW</code> - Currently focused window</li>
<li><code>_NET_WM_WINDOW_TYPE</code> - Window type classification</li>
</ul>
<h4>Implementation</h4>
<p style="color: var(--text-muted);">
<code>atoms.c</code> manages X11 atom creation and EWMH property updates.
Properties are updated on window focus changes, workspace switches, and state changes.
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/wm/latest/" target="_blank">Extended Window Manager Hints Specification</a> - freedesktop.org</li>
<li><a href="https://en.wikipedia.org/wiki/Extended_Window_Manager_Hints" target="_blank">Extended Window Manager Hints</a> - Wikipedia</li>
</ul>
</div>
<h3 id="icccm">Inter-Client Communication Conventions Manual (ICCCM)</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Define low-level conventions for X11 client communication, including
selections, window management, and session management.</p>
<h4>Key Features Implemented</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>WM_PROTOCOLS</code> - Window close handling</li>
<li><code>WM_DELETE_WINDOW</code> - Graceful window closing</li>
<li><code>WM_NAME</code> / <code>_NET_WM_NAME</code> - Window titles</li>
<li><code>WM_CLASS</code> - Application classification</li>
<li><code>WM_HINTS</code> - Window hints (urgency, input model)</li>
</ul>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html" target="_blank">Inter-Client Communication Conventions Manual</a> - X.Org</li>
<li><a href="https://tronche.com/gui/x/icccm/" target="_blank">ICCCM Reference</a> - Christophe Tronche</li>
</ul>
</div>
<h3 id="xembed">XEmbed Protocol</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Enable embedding of controls from one application into another,
forming the basis of the system tray implementation.</p>
<h4>How It Works</h4>
<p>The embedder (DWN panel) acts as a window manager for embedded clients.
Client windows are reparented into the embedder, and events are coordinated
through XEMBED messages.</p>
<h4>Key Messages</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>XEMBED_EMBEDDED_NOTIFY</code> - Sent when embedding completes</li>
<li><code>XEMBED_FOCUS_IN/OUT</code> - Focus coordination</li>
<li><code>XEMBED_WINDOW_ACTIVATE</code> - Window activation</li>
</ul>
<h4>Implementation</h4>
<p style="color: var(--text-muted);">
<code>systray.c</code> implements the embedder side, reparenting tray icons
and forwarding click events.
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/xembed-spec/latest/" target="_blank">XEmbed Protocol Specification</a> - freedesktop.org</li>
<li><a href="https://www.freedesktop.org/wiki/Specifications/xembed-spec/" target="_blank">XEmbed Spec Wiki</a> - freedesktop.org</li>
</ul>
</div>
<h3 id="systray">System Tray Protocol</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Enable applications to display status icons in a desktop panel,
providing a standardized notification area.</p>
<h4>How It Works</h4>
<p>DWN acquires the <code>_NET_SYSTEM_TRAY_S0</code> selection to become the
tray manager. Applications send <code>SYSTEM_TRAY_REQUEST_DOCK</code> messages
to dock their icons.</p>
<h4>Docking Process</h4>
<ol style="padding-left: 1.25rem; color: var(--text-muted);">
<li>DWN acquires <code>_NET_SYSTEM_TRAY_S0</code> selection</li>
<li>Application sends <code>SYSTEM_TRAY_REQUEST_DOCK</code> client message</li>
<li>DWN creates embedding window and reparents icon</li>
<li>DWN sends <code>XEMBED_EMBEDDED_NOTIFY</code> to icon</li>
<li>Click events forwarded to icon window</li>
</ol>
<h4>Supported Applications</h4>
<p style="color: var(--text-muted);">
nm-applet, blueman-applet, Telegram, pasystray, udiskie, and any XEmbed-compatible tray icon.
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/systemtray-spec/systemtray-spec-0.3.html" target="_blank">System Tray Protocol Specification</a> - freedesktop.org</li>
</ul>
</div>
<h2 id="xdg" style="margin-top: 3rem;">XDG Specifications</h2>
<h3>Desktop Entry Specification</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Standard format for application metadata files (.desktop files) used
by application launchers and autostart systems.</p>
<h4>Key Fields Parsed</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>Exec</code> - Command to execute</li>
<li><code>TryExec</code> - Check if binary exists</li>
<li><code>Hidden</code> - Entry is disabled</li>
<li><code>OnlyShowIn</code> / <code>NotShowIn</code> - Desktop environment filters</li>
<li><code>Terminal</code> - Run in terminal</li>
</ul>
<h4>Implementation</h4>
<p style="color: var(--text-muted);">
<code>applauncher.c</code> parses .desktop files for the application menu.
<code>autostart.c</code> parses autostart entries.
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/desktop-entry-spec/latest/" target="_blank">Desktop Entry Specification</a> - freedesktop.org</li>
<li><a href="https://wiki.archlinux.org/title/Desktop_entries" target="_blank">Desktop Entries</a> - ArchWiki</li>
</ul>
</div>
<h3>Desktop Application Autostart Specification</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Define standard locations and format for applications that should
start automatically when the user logs in.</p>
<h4>Directories Scanned</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>/etc/xdg/autostart/</code> - System-wide autostart</li>
<li><code>~/.config/autostart/</code> - User autostart</li>
<li><code>~/.config/dwn/autostart.d/</code> - DWN-specific (symlinks)</li>
</ul>
<h4>Key Behavior</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li>User entries override system entries with same filename</li>
<li><code>Hidden=true</code> disables an entry</li>
<li><code>TryExec</code> prevents running if binary missing</li>
</ul>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html" target="_blank">Desktop Application Autostart Specification</a> - freedesktop.org</li>
<li><a href="https://wiki.archlinux.org/title/XDG_Autostart" target="_blank">XDG Autostart</a> - ArchWiki</li>
</ul>
</div>
<h3>Desktop Notifications Specification</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Standard D-Bus interface for applications to display passive notifications
to users without blocking.</p>
<h4>D-Bus Interface</h4>
<p style="color: var(--text-muted);">
DWN implements <code>org.freedesktop.Notifications</code> on the session bus
at path <code>/org/freedesktop/Notifications</code>.
</p>
<h4>Key Methods</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>Notify</code> - Display a notification</li>
<li><code>CloseNotification</code> - Dismiss a notification</li>
<li><code>GetCapabilities</code> - Query supported features</li>
<li><code>GetServerInformation</code> - Server metadata</li>
</ul>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/notification-spec/latest/" target="_blank">Desktop Notifications Specification</a> - freedesktop.org</li>
<li><a href="https://wiki.archlinux.org/title/Desktop_notifications" target="_blank">Desktop Notifications</a> - ArchWiki</li>
</ul>
</div>
<h2 id="async" style="margin-top: 3rem;">Async Programming Patterns</h2>
<h3>libcurl Multi Interface</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Perform HTTP requests asynchronously without blocking the main event loop,
essential for AI features and news fetching.</p>
<h4>How It Works</h4>
<p>Instead of blocking on network I/O, the multi interface allows the main
loop to poll for completion. <code>curl_multi_perform()</code> advances
transfers incrementally.</p>
<div class="code-header">
<span>Integration Pattern</span>
</div>
<pre><code>// In event loop (16ms intervals)
curl_multi_perform(multi_handle, &running);
CURLMsg *msg = curl_multi_info_read(multi_handle, &msgs_left);
if (msg && msg->msg == CURLMSG_DONE) {
// Request completed, process response
}</code></pre>
<h4>Implementation</h4>
<p style="color: var(--text-muted);">
<code>ai.c</code> uses curl_multi for OpenRouter API calls.
Responses processed in <code>ai_process_pending()</code> called from main loop.
</p>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://curl.se/libcurl/c/libcurl-multi.html" target="_blank">libcurl multi interface overview</a> - curl.se</li>
<li><a href="https://curl.se/libcurl/c/libcurl-tutorial.html" target="_blank">libcurl programming tutorial</a> - curl.se</li>
<li><a href="https://curl.se/libcurl/c/multi-app.html" target="_blank">multi-app.c example</a> - curl.se</li>
</ul>
</div>
<h3>pthread for Background I/O</h3>
<div class="card" style="margin-bottom: 2rem;">
<h4>Purpose</h4>
<p>Offload blocking operations to separate threads when async APIs
are unavailable or impractical.</p>
<h4>Implementation</h4>
<p style="color: var(--text-muted);">
<code>news.c</code> spawns a detached thread for RSS fetching.
Mutex guards shared state, atomic flags prevent concurrent fetches.
</p>
<div class="code-header">
<span>Pattern</span>
</div>
<pre><code>static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER;
static atomic_int fetch_running = 0;
void news_fetch_async(void) {
if (atomic_load(&fetch_running)) return;
atomic_store(&fetch_running, 1);
pthread_create(&fetch_thread, NULL, fetch_thread_func, NULL);
}</code></pre>
</div>
<h2 id="modular" style="margin-top: 3rem;">Modular Architecture</h2>
<div class="card" style="margin-bottom: 2rem;">
<h4>Design Principle</h4>
<p>Each module has a single responsibility with well-defined interfaces.
Modules communicate through the global <code>dwn</code> state and
function calls, never through shared mutable state.</p>
<h4>Module Structure</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><code>include/module.h</code> - Public API declarations</li>
<li><code>src/module.c</code> - Private implementation</li>
<li>Static functions for internal logic</li>
<li>module_init() / module_cleanup() lifecycle</li>
</ul>
<h4>Module Responsibilities</h4>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Module</th>
<th>Responsibility</th>
</tr>
</thead>
<tbody>
<tr><td>main.c</td><td>Event loop, initialization orchestration</td></tr>
<tr><td>client.c</td><td>Window management, focus handling</td></tr>
<tr><td>workspace.c</td><td>Virtual desktop management</td></tr>
<tr><td>layout.c</td><td>Tiling algorithms</td></tr>
<tr><td>panel.c</td><td>UI panels and widgets</td></tr>
<tr><td>systray.c</td><td>System tray protocol</td></tr>
<tr><td>notifications.c</td><td>D-Bus notification daemon</td></tr>
<tr><td>autostart.c</td><td>XDG autostart support</td></tr>
<tr><td>config.c</td><td>Configuration parsing</td></tr>
<tr><td>ai.c</td><td>AI integration</td></tr>
</tbody>
</table>
</div>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://en.wikipedia.org/wiki/Modular_programming" target="_blank">Modular Programming</a> - Wikipedia</li>
<li><a href="https://en.wikipedia.org/wiki/Separation_of_concerns" target="_blank">Separation of Concerns</a> - Wikipedia</li>
<li><a href="https://thecloudstrap.com/chapter-14-modular-programming-in-c/" target="_blank">Modular Programming in C</a> - TheCloudStrap</li>
</ul>
</div>
<h2 id="defensive" style="margin-top: 3rem;">Defensive Programming</h2>
<div class="card" style="margin-bottom: 2rem;">
<h4>Core Principles</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><strong>Null Checks</strong> - All pointer parameters validated before use</li>
<li><strong>Bounds Checking</strong> - Array indices and string lengths verified</li>
<li><strong>Return Value Checking</strong> - malloc(), fopen(), etc. checked for failure</li>
<li><strong>String Safety</strong> - strncpy() with size-1, explicit null termination</li>
<li><strong>Assertions</strong> - assert() for programmer errors in debug builds</li>
</ul>
<h4>Examples in DWN</h4>
<div class="code-header">
<span>Null Check Pattern</span>
</div>
<pre><code>const char *config_get_terminal(void) {
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->terminal;
}
return "xterm"; // Safe fallback
}</code></pre>
<div class="code-header">
<span>String Safety Pattern</span>
</div>
<pre><code>strncpy(cfg->terminal, value, sizeof(cfg->terminal) - 1);
cfg->terminal[sizeof(cfg->terminal) - 1] = '\0'; // Guarantee null termination</code></pre>
<h4>Research Sources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://wiki.sei.cmu.edu/confluence/display/c/API00-C.+Functions+should+validate+their+parameters" target="_blank">API00-C: Functions should validate their parameters</a> - SEI CERT</li>
<li><a href="https://enterprisecraftsmanship.com/posts/defensive-programming/" target="_blank">Defensive programming: the good, the bad and the ugly</a> - Enterprise Craftsmanship</li>
<li><a href="https://www.cse.psu.edu/~gxt29/teaching/cs447s19/slides/05defensiveProg.pdf" target="_blank">Defensive Programming</a> - Penn State (PDF)</li>
</ul>
</div>
<h2 id="sources" style="margin-top: 3rem;">Complete Research Sources</h2>
<div class="card">
<h4>C Design Patterns</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://interrupt.memfault.com/blog/opaque-pointers" target="_blank">Practical Design Patterns: Opaque Pointers and Objects in C</a> - Memfault</li>
<li><a href="https://wiki.sei.cmu.edu/confluence/display/c/" target="_blank">SEI CERT C Coding Standard</a> - Carnegie Mellon University</li>
<li><a href="https://refactoring.guru/design-patterns" target="_blank">Design Patterns Catalog</a> - Refactoring Guru</li>
<li><a href="https://sourcemaking.com/design_patterns" target="_blank">Design Patterns</a> - SourceMaking</li>
<li><a href="https://embeddedartistry.com/fieldatlas/" target="_blank">Embedded Artistry Field Atlas</a> - Embedded Artistry</li>
<li><a href="https://www.state-machine.com/doc/AN_Simple_OOP_in_C.pdf" target="_blank">Object-Oriented Programming in C</a> - Quantum Leaps</li>
</ul>
<h4>X11 and freedesktop.org</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://specifications.freedesktop.org/wm/latest/" target="_blank">Extended Window Manager Hints (EWMH)</a> - freedesktop.org</li>
<li><a href="https://specifications.freedesktop.org/xembed-spec/latest/" target="_blank">XEmbed Protocol</a> - freedesktop.org</li>
<li><a href="https://specifications.freedesktop.org/systemtray-spec/systemtray-spec-0.3.html" target="_blank">System Tray Protocol</a> - freedesktop.org</li>
<li><a href="https://specifications.freedesktop.org/notification-spec/latest/" target="_blank">Desktop Notifications</a> - freedesktop.org</li>
<li><a href="https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html" target="_blank">Desktop Application Autostart</a> - freedesktop.org</li>
<li><a href="https://specifications.freedesktop.org/desktop-entry-spec/latest/" target="_blank">Desktop Entry Specification</a> - freedesktop.org</li>
</ul>
<h4>Libraries and APIs</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://curl.se/libcurl/c/libcurl-multi.html" target="_blank">libcurl multi interface</a> - curl.se</li>
<li><a href="https://dbus.freedesktop.org/doc/dbus-specification.html" target="_blank">D-Bus Specification</a> - freedesktop.org</li>
<li><a href="https://www.x.org/releases/current/doc/" target="_blank">X Window System Documentation</a> - X.Org</li>
</ul>
<h4>Unix Programming</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://0xjet.github.io/3OHA/2022/04/11/post.html" target="_blank">UNIX daemonization and the double fork</a> - Juan Tapiador</li>
<li><a href="https://eli.thegreenplace.net/2009/04/27/using-goto-for-error-handling-in-c" target="_blank">Using goto for error handling in C</a> - Eli Bendersky</li>
<li><a href="https://gameprogrammingpatterns.com/" target="_blank">Game Programming Patterns</a> - Robert Nystrom</li>
</ul>
<h4>Community Resources</h4>
<ul style="padding-left: 1.25rem; color: var(--text-muted);">
<li><a href="https://wiki.archlinux.org/" target="_blank">ArchWiki</a> - Comprehensive Linux documentation</li>
<li><a href="https://en.wikipedia.org/" target="_blank">Wikipedia</a> - Design pattern overviews</li>
</ul>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Source Code</a></li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn/issues">Issue Tracker</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Author</h4>
<p style="color: var(--text-muted);">
retoor &lt;retoor@molodetz.nl&gt;
</p>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -366,6 +367,7 @@
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -168,7 +169,42 @@
at 80 pixels per second keeps you informed without distraction. at 80 pixels per second keeps you informed without distraction.
</p> </p>
</div> </div>
<h3 style="margin-top: 3rem; margin-bottom: 1.5rem;">System Tray Features</h3> <h3 style="margin-top: 3rem; margin-bottom: 1.5rem;">XEmbed System Tray</h3>
<div class="card" style="margin-bottom: 2rem;">
<h3>&#128241; External Application Icons</h3>
<p>DWN implements the freedesktop.org XEmbed System Tray protocol, allowing external applications
to dock their status icons in the panel - just like XFCE, GNOME, or KDE.</p>
<p style="margin-top: 1rem; color: var(--text-muted);">Supported applications include:</p>
<ul style="margin-top: 0.5rem; padding-left: 1.25rem; color: var(--text-muted);">
<li><strong>Telegram</strong> - Notification icons for messages</li>
<li><strong>nm-applet</strong> - NetworkManager GUI</li>
<li><strong>blueman-applet</strong> - Bluetooth manager</li>
<li><strong>pasystray</strong> - PulseAudio control</li>
<li><strong>udiskie</strong> - USB automounter</li>
<li>Any application with tray icon support</li>
</ul>
<p style="margin-top: 1rem; color: var(--text-muted);">
Simply launch any tray-enabled application and its icon will automatically appear in the panel.
Click on icons to interact - all events are forwarded to the application.
</p>
</div>
<h3 style="margin-top: 3rem; margin-bottom: 1.5rem;">XDG Autostart</h3>
<div class="card" style="margin-bottom: 2rem;">
<h3>Automatic Application Startup</h3>
<p>DWN follows the XDG Autostart specification, automatically starting system services
and tray applications - just like traditional desktop environments.</p>
<p style="margin-top: 1rem; color: var(--text-muted);">Directories scanned at startup:</p>
<ul style="margin-top: 0.5rem; padding-left: 1.25rem; color: var(--text-muted);">
<li><code>/etc/xdg/autostart/</code> - System defaults (nm-applet, blueman, power-manager)</li>
<li><code>~/.config/autostart/</code> - User XDG autostart entries</li>
<li><code>~/.config/dwn/autostart.d/</code> - DWN-specific symlinks and scripts</li>
</ul>
<p style="margin-top: 1rem; color: var(--text-muted);">
All applications launch concurrently for fastest boot time. Properly handles .desktop
file fields including Hidden, TryExec, OnlyShowIn, and NotShowIn.
</p>
</div>
<h3 style="margin-top: 2rem; margin-bottom: 1.5rem;">Built-in Widgets</h3>
<div class="features-grid"> <div class="features-grid">
<div class="card"> <div class="card">
<h3>&#128267; Battery Monitor</h3> <h3>&#128267; Battery Monitor</h3>
@ -336,6 +372,37 @@
</div> </div>
</div> </div>
</section> </section>
<section class="section">
<div class="container">
<h2>Learning DWN</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
Two built-in modes help you learn DWN quickly: an interactive tutorial and
an automated demo that showcases all features.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3><span class="card-icon">&#128218;</span> Interactive Tutorial</h3>
<p>Press <kbd>Super</kbd> + <kbd>T</kbd> to start a hands-on tutorial that:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Guides you through essential shortcuts step-by-step</li>
<li>Waits for you to press the correct key combination</li>
<li>Automatically advances when you complete each step</li>
<li>Can be restarted at any time</li>
</ul>
</div>
<div class="card">
<h3><span class="card-icon">&#127916;</span> Demo Mode</h3>
<p>Press <kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>D</kbd> for an automated showcase:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Demonstrates window management, workspaces, and layouts</li>
<li>Shows panel features and system tray</li>
<li>Highlights AI integration and news ticker</li>
<li>Displays complete keyboard shortcut reference</li>
</ul>
</div>
</div>
</div>
</section>
<section class="section section-alt"> <section class="section section-alt">
<div class="container" style="text-align: center;"> <div class="container" style="text-align: center;">
<h2>Ready to Try DWN?</h2> <h2>Ready to Try DWN?</h2>
@ -366,6 +433,7 @@
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -27,6 +27,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -93,8 +94,14 @@
<div class="feature-card"> <div class="feature-card">
<div class="feature-icon">&#128187;</div> <div class="feature-icon">&#128187;</div>
<h3>System Tray</h3> <h3>System Tray</h3>
<p>Integrated system tray with battery, volume, and WiFi indicators. <p>XEmbed protocol for external app icons (Telegram, Bluetooth, etc.) plus
Volume slider and network selection dropdowns included.</p> built-in battery, volume, and WiFi widgets with interactive controls.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#9889;</div>
<h3>XDG Autostart</h3>
<p>Automatic startup of system services and tray applications following the
XDG Autostart spec. Works with nm-applet, blueman, and more.</p>
</div> </div>
</div> </div>
</div> </div>
@ -249,6 +256,7 @@ echo "exec dwn" >> ~/.xinitrc</code></pre>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -528,6 +529,7 @@ sudo dnf install dejavu-fonts-all liberation-fonts # Fedora</code></pre>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -26,6 +26,7 @@
<a href="configuration.html">Configuration</a> <a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a> <a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a> <a href="architecture.html">Architecture</a>
<a href="design-patterns.html">Design Patterns</a>
</div> </div>
</li> </li>
<li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li> <li><a href="https://retoor.molodetz.nl/retoor/dwn">Git</a></li>
@ -107,6 +108,10 @@
<td><kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td> <td><kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Cycle to previous window</td> <td>Cycle to previous window</td>
</tr> </tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F9</kbd></td>
<td>Toggle minimize/restore</td>
</tr>
<tr> <tr>
<td><kbd>Alt</kbd> + <kbd>F10</kbd></td> <td><kbd>Alt</kbd> + <kbd>F10</kbd></td>
<td>Toggle maximize</td> <td>Toggle maximize</td>
@ -245,6 +250,14 @@
<td><kbd>Super</kbd> + <kbd>D</kbd></td> <td><kbd>Super</kbd> + <kbd>D</kbd></td>
<td>Decrease master window count</td> <td>Decrease master window count</td>
</tr> </tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Left</kbd></td>
<td>Snap window to left half (50% width)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Right</kbd></td>
<td>Snap window to right half (50% width)</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -322,6 +335,10 @@
<td><kbd>Super</kbd> + <kbd>T</kbd></td> <td><kbd>Super</kbd> + <kbd>T</kbd></td>
<td>Start/continue interactive tutorial</td> <td>Start/continue interactive tutorial</td>
</tr> </tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>D</kbd></td>
<td>Start/stop demo mode</td>
</tr>
<tr> <tr>
<td><kbd>Super</kbd> + <kbd>Backspace</kbd></td> <td><kbd>Super</kbd> + <kbd>Backspace</kbd></td>
<td>Quit DWN</td> <td>Quit DWN</td>
@ -363,10 +380,10 @@
<kbd>Super</kbd>+<kbd>H</kbd>/<kbd>L</kbd> Resize master <kbd>Super</kbd>+<kbd>H</kbd>/<kbd>L</kbd> Resize master
</li> </li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);"> <li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Shift</kbd>+<kbd>F1-9</kbd> Move to workspace <kbd>Super</kbd>+<kbd>Left</kbd>/<kbd>Right</kbd> Snap 50%
</li> </li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);"> <li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Super</kbd>+<kbd>Shift</kbd>+<kbd>A</kbd> AI command <kbd>Shift</kbd>+<kbd>F1-9</kbd> Move to workspace
</li> </li>
<li style="padding: 0.5rem 0;"> <li style="padding: 0.5rem 0;">
<kbd>Super</kbd>+<kbd>S</kbd> Show shortcuts <kbd>Super</kbd>+<kbd>S</kbd> Show shortcuts
@ -395,6 +412,7 @@
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li> <li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li> <li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li> <li><a href="architecture.html">Architecture</a></li>
<li><a href="design-patterns.html">Design Patterns</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@ -16,12 +16,17 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
#define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions" #define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions"
static AIRequest *request_queue = NULL; static AIRequest *request_queue = NULL;
static CURLM *curl_multi = NULL; static CURLM *curl_multi = NULL;
static AIContext current_context; static AIContext current_context;
static pthread_mutex_t curl_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
static atomic_int ai_shutting_down = 0;
typedef struct { typedef struct {
char *data; char *data;
@ -54,6 +59,8 @@ bool ai_init(void)
return false; return false;
} }
atomic_store(&ai_shutting_down, 0);
if (dwn->config->openrouter_api_key[0] == '\0') { if (dwn->config->openrouter_api_key[0] == '\0') {
LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)"); LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)");
dwn->ai_enabled = false; dwn->ai_enabled = false;
@ -61,12 +68,15 @@ bool ai_init(void)
} }
curl_global_init(CURL_GLOBAL_DEFAULT); curl_global_init(CURL_GLOBAL_DEFAULT);
curl_multi = curl_multi_init();
pthread_mutex_lock(&curl_mutex);
curl_multi = curl_multi_init();
if (curl_multi == NULL) { if (curl_multi == NULL) {
pthread_mutex_unlock(&curl_mutex);
LOG_ERROR("Failed to initialize curl multi handle"); LOG_ERROR("Failed to initialize curl multi handle");
return false; return false;
} }
pthread_mutex_unlock(&curl_mutex);
dwn->ai_enabled = true; dwn->ai_enabled = true;
LOG_INFO("AI features enabled"); LOG_INFO("AI features enabled");
@ -76,6 +86,9 @@ bool ai_init(void)
void ai_cleanup(void) void ai_cleanup(void)
{ {
atomic_store(&ai_shutting_down, 1);
pthread_mutex_lock(&queue_mutex);
while (request_queue != NULL) { while (request_queue != NULL) {
AIRequest *next = request_queue->next; AIRequest *next = request_queue->next;
if (request_queue->prompt) free(request_queue->prompt); if (request_queue->prompt) free(request_queue->prompt);
@ -83,11 +96,14 @@ void ai_cleanup(void)
free(request_queue); free(request_queue);
request_queue = next; request_queue = next;
} }
pthread_mutex_unlock(&queue_mutex);
pthread_mutex_lock(&curl_mutex);
if (curl_multi != NULL) { if (curl_multi != NULL) {
curl_multi_cleanup(curl_multi); curl_multi_cleanup(curl_multi);
curl_multi = NULL; curl_multi = NULL;
} }
pthread_mutex_unlock(&curl_mutex);
curl_global_cleanup(); curl_global_cleanup();
} }
@ -104,6 +120,10 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
return NULL; return NULL;
} }
if (atomic_load(&ai_shutting_down)) {
return NULL;
}
AIRequest *req = dwn_calloc(1, sizeof(AIRequest)); AIRequest *req = dwn_calloc(1, sizeof(AIRequest));
req->prompt = dwn_strdup(prompt); req->prompt = dwn_strdup(prompt);
req->state = AI_STATE_PENDING; req->state = AI_STATE_PENDING;
@ -162,16 +182,21 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
curl_multi_add_handle(curl_multi, easy); pthread_mutex_lock(&curl_mutex);
if (curl_multi != NULL) {
curl_multi_add_handle(curl_multi, easy);
}
pthread_mutex_unlock(&curl_mutex);
pthread_mutex_lock(&queue_mutex);
req->next = request_queue; req->next = request_queue;
request_queue = req; request_queue = req;
pthread_mutex_unlock(&queue_mutex);
req->user_data = response; req->user_data = response;
LOG_DEBUG("AI request sent: %.50s...", prompt); LOG_DEBUG("AI request sent: %.50s...", prompt);
return req; return req;
} }
@ -181,6 +206,7 @@ void ai_cancel_request(AIRequest *req)
return; return;
} }
pthread_mutex_lock(&queue_mutex);
AIRequest **pp = &request_queue; AIRequest **pp = &request_queue;
while (*pp != NULL) { while (*pp != NULL) {
if (*pp == req) { if (*pp == req) {
@ -189,6 +215,7 @@ void ai_cancel_request(AIRequest *req)
} }
pp = &(*pp)->next; pp = &(*pp)->next;
} }
pthread_mutex_unlock(&queue_mutex);
req->state = AI_STATE_ERROR; req->state = AI_STATE_ERROR;
@ -204,7 +231,13 @@ void ai_cancel_request(AIRequest *req)
void ai_process_pending(void) void ai_process_pending(void)
{ {
if (atomic_load(&ai_shutting_down)) {
return;
}
pthread_mutex_lock(&curl_mutex);
if (curl_multi == NULL) { if (curl_multi == NULL) {
pthread_mutex_unlock(&curl_mutex);
return; return;
} }
@ -277,6 +310,7 @@ void ai_process_pending(void)
curl_easy_cleanup(easy); curl_easy_cleanup(easy);
} }
} }
pthread_mutex_unlock(&curl_mutex);
} }
@ -586,6 +620,10 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
return NULL; return NULL;
} }
if (atomic_load(&ai_shutting_down)) {
return NULL;
}
ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest)); ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest));
req->query = dwn_strdup(query); req->query = dwn_strdup(query);
req->state = AI_STATE_PENDING; req->state = AI_STATE_PENDING;
@ -638,13 +676,20 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
pthread_mutex_lock(&curl_mutex);
if (curl_multi == NULL) { if (curl_multi == NULL) {
curl_multi = curl_multi_init(); curl_multi = curl_multi_init();
} }
curl_multi_add_handle(curl_multi, easy); if (curl_multi != NULL) {
curl_multi_add_handle(curl_multi, easy);
}
pthread_mutex_unlock(&curl_mutex);
pthread_mutex_lock(&queue_mutex);
req->next = exa_queue; req->next = exa_queue;
exa_queue = req; exa_queue = req;
pthread_mutex_unlock(&queue_mutex);
req->user_data = response; req->user_data = response;
LOG_DEBUG("Exa search sent: %s", query); LOG_DEBUG("Exa search sent: %s", query);
@ -654,10 +699,24 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
void exa_process_pending(void) void exa_process_pending(void)
{ {
if (curl_multi == NULL || exa_queue == NULL) { if (atomic_load(&ai_shutting_down)) {
return; return;
} }
pthread_mutex_lock(&curl_mutex);
if (curl_multi == NULL) {
pthread_mutex_unlock(&curl_mutex);
return;
}
pthread_mutex_lock(&queue_mutex);
if (exa_queue == NULL) {
pthread_mutex_unlock(&queue_mutex);
pthread_mutex_unlock(&curl_mutex);
return;
}
pthread_mutex_unlock(&queue_mutex);
int running_handles; int running_handles;
curl_multi_perform(curl_multi, &running_handles); curl_multi_perform(curl_multi, &running_handles);
@ -671,6 +730,7 @@ void exa_process_pending(void)
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req); curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
pthread_mutex_lock(&queue_mutex);
bool is_exa = false; bool is_exa = false;
for (ExaRequest *r = exa_queue; r != NULL; r = r->next) { for (ExaRequest *r = exa_queue; r != NULL; r = r->next) {
if (r == req) { if (r == req) {
@ -678,6 +738,7 @@ void exa_process_pending(void)
break; break;
} }
} }
pthread_mutex_unlock(&queue_mutex);
if (is_exa && req != NULL) { if (is_exa && req != NULL) {
ResponseBuffer *buf = (ResponseBuffer *)req->user_data; ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
@ -699,6 +760,7 @@ void exa_process_pending(void)
dwn_free(buf); dwn_free(buf);
} }
pthread_mutex_lock(&queue_mutex);
ExaRequest **pp = &exa_queue; ExaRequest **pp = &exa_queue;
while (*pp != NULL) { while (*pp != NULL) {
if (*pp == req) { if (*pp == req) {
@ -707,12 +769,14 @@ void exa_process_pending(void)
} }
pp = &(*pp)->next; pp = &(*pp)->next;
} }
pthread_mutex_unlock(&queue_mutex);
} }
curl_multi_remove_handle(curl_multi, easy); curl_multi_remove_handle(curl_multi, easy);
curl_easy_cleanup(easy); curl_easy_cleanup(easy);
} }
} }
pthread_mutex_unlock(&curl_mutex);
} }
static void exa_launcher_callback(ExaRequest *req) static void exa_launcher_callback(ExaRequest *req)

View File

@ -87,6 +87,8 @@ void atoms_init(Display *display)
ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE"); ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE");
ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0"); ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0");
ewmh.NET_SYSTEM_TRAY_ORIENTATION = ATOM("_NET_SYSTEM_TRAY_ORIENTATION");
ewmh.NET_SYSTEM_TRAY_VISUAL = ATOM("_NET_SYSTEM_TRAY_VISUAL");
ewmh.MANAGER = ATOM("MANAGER"); ewmh.MANAGER = ATOM("MANAGER");
ewmh.XEMBED = ATOM("_XEMBED"); ewmh.XEMBED = ATOM("_XEMBED");
ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO"); ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO");
@ -437,3 +439,74 @@ void atoms_send_client_message(Window window, Atom message_type,
XSendEvent(dwn->display, dwn->root, False, XSendEvent(dwn->display, dwn->root, False,
SubstructureNotifyMask | SubstructureRedirectMask, &ev); SubstructureNotifyMask | SubstructureRedirectMask, &ev);
} }
bool atoms_update_wm_state(Window window, Atom state, bool add)
{
if (dwn == NULL || dwn->display == NULL || window == None) {
return false;
}
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
Atom *states = NULL;
int state_count = 0;
if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_STATE,
0, 32, False, XA_ATOM,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) == Success && data != NULL) {
states = (Atom *)data;
state_count = (int)nitems;
}
bool found = false;
int found_index = -1;
for (int i = 0; i < state_count; i++) {
if (states[i] == state) {
found = true;
found_index = i;
break;
}
}
if (add && found) {
if (data) XFree(data);
return true;
}
if (!add && !found) {
if (data) XFree(data);
return true;
}
Atom new_states[32];
int new_count = 0;
if (add) {
for (int i = 0; i < state_count && new_count < 31; i++) {
new_states[new_count++] = states[i];
}
if (new_count < 32) {
new_states[new_count++] = state;
}
} else {
for (int i = 0; i < state_count && new_count < 32; i++) {
if (i != found_index) {
new_states[new_count++] = states[i];
}
}
}
if (data) XFree(data);
if (new_count > 0) {
XChangeProperty(dwn->display, window, ewmh.NET_WM_STATE,
XA_ATOM, 32, PropModeReplace,
(unsigned char *)new_states, new_count);
} else {
XDeleteProperty(dwn->display, window, ewmh.NET_WM_STATE);
}
return true;
}

290
src/autostart.c Normal file
View File

@ -0,0 +1,290 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* XDG Autostart support
*/
#include "autostart.h"
#include "dwn.h"
#include "config.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#include <limits.h>
#include <errno.h>
static void scan_xdg_directory(const char *dir_path);
static void scan_symlink_directory(const char *dir_path);
static int parse_desktop_file(const char *path, char *exec_cmd, size_t exec_size);
static void strip_exec_field_codes(char *exec);
void autostart_init(void)
{
if (dwn == NULL || dwn->config == NULL) {
return;
}
if (!dwn->config->autostart_enabled) {
LOG_DEBUG("Autostart disabled in config");
return;
}
if (dwn->config->autostart_path[0] != '\0') {
struct stat st;
if (stat(dwn->config->autostart_path, &st) != 0) {
if (mkdir(dwn->config->autostart_path, 0755) == 0) {
LOG_INFO("Created autostart directory: %s", dwn->config->autostart_path);
}
}
}
LOG_INFO("Autostart initialized");
}
void autostart_run(void)
{
if (dwn == NULL || dwn->config == NULL) {
return;
}
if (!dwn->config->autostart_enabled) {
return;
}
LOG_INFO("Running autostart applications");
if (dwn->config->autostart_xdg) {
scan_xdg_directory("/etc/xdg/autostart");
char *user_autostart = expand_path("~/.config/autostart");
if (user_autostart != NULL) {
scan_xdg_directory(user_autostart);
free(user_autostart);
}
}
if (dwn->config->autostart_path[0] != '\0') {
scan_symlink_directory(dwn->config->autostart_path);
}
LOG_INFO("Autostart complete");
}
void autostart_cleanup(void)
{
LOG_DEBUG("Autostart cleanup");
}
static void scan_xdg_directory(const char *dir_path)
{
if (dir_path == NULL) {
return;
}
DIR *dir = opendir(dir_path);
if (dir == NULL) {
LOG_DEBUG("Cannot open autostart directory: %s", dir_path);
return;
}
LOG_DEBUG("Scanning XDG autostart directory: %s", dir_path);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
if (!str_ends_with(entry->d_name, ".desktop")) {
continue;
}
char full_path[PATH_MAX];
int ret = snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
if (ret < 0 || (size_t)ret >= sizeof(full_path)) {
continue;
}
char exec_cmd[512];
if (parse_desktop_file(full_path, exec_cmd, sizeof(exec_cmd)) == 0) {
LOG_INFO("Autostart: %s -> %s", entry->d_name, exec_cmd);
spawn_async(exec_cmd);
}
}
closedir(dir);
}
static void scan_symlink_directory(const char *dir_path)
{
if (dir_path == NULL) {
return;
}
DIR *dir = opendir(dir_path);
if (dir == NULL) {
LOG_DEBUG("Cannot open symlink directory: %s", dir_path);
return;
}
LOG_DEBUG("Scanning symlink directory: %s", dir_path);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
char full_path[PATH_MAX];
int ret = snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
if (ret < 0 || (size_t)ret >= sizeof(full_path)) {
continue;
}
if (access(full_path, X_OK) != 0) {
LOG_DEBUG("Skipping non-executable: %s", entry->d_name);
continue;
}
LOG_INFO("Autostart (symlink): %s", entry->d_name);
spawn_async(full_path);
}
closedir(dir);
}
static int parse_desktop_file(const char *path, char *exec_cmd, size_t exec_size)
{
if (path == NULL || exec_cmd == NULL || exec_size == 0) {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
char line[1024];
int in_desktop_entry = 0;
int hidden = 0;
int enabled = 1;
char try_exec[256] = {0};
exec_cmd[0] = '\0';
while (fgets(line, sizeof(line), f) != NULL) {
size_t len = strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) {
line[--len] = '\0';
}
char *trimmed = str_trim(line);
if (trimmed[0] == '\0' || trimmed[0] == '#') {
continue;
}
if (strcmp(trimmed, "[Desktop Entry]") == 0) {
in_desktop_entry = 1;
continue;
}
if (trimmed[0] == '[') {
in_desktop_entry = 0;
continue;
}
if (!in_desktop_entry) {
continue;
}
if (strcmp(trimmed, "Hidden=true") == 0) {
hidden = 1;
} else if (strcmp(trimmed, "X-GNOME-Autostart-enabled=false") == 0) {
enabled = 0;
} else if (strncmp(trimmed, "TryExec=", 8) == 0) {
strncpy(try_exec, trimmed + 8, sizeof(try_exec) - 1);
try_exec[sizeof(try_exec) - 1] = '\0';
} else if (strncmp(trimmed, "Exec=", 5) == 0) {
strncpy(exec_cmd, trimmed + 5, exec_size - 1);
exec_cmd[exec_size - 1] = '\0';
} else if (strncmp(trimmed, "NotShowIn=", 10) == 0) {
if (strstr(trimmed + 10, "DWN") != NULL) {
hidden = 1;
}
} else if (strncmp(trimmed, "OnlyShowIn=", 11) == 0) {
if (strstr(trimmed + 11, "DWN") == NULL) {
hidden = 1;
}
}
}
fclose(f);
if (hidden || !enabled || exec_cmd[0] == '\0') {
exec_cmd[0] = '\0';
return -1;
}
if (try_exec[0] != '\0') {
char *space = strchr(try_exec, ' ');
if (space != NULL) {
*space = '\0';
}
if (access(try_exec, X_OK) != 0) {
char which_cmd[512];
snprintf(which_cmd, sizeof(which_cmd), "which %s >/dev/null 2>&1", try_exec);
if (system(which_cmd) != 0) {
exec_cmd[0] = '\0';
return -1;
}
}
}
strip_exec_field_codes(exec_cmd);
return 0;
}
static void strip_exec_field_codes(char *exec)
{
if (exec == NULL) {
return;
}
char result[512];
size_t j = 0;
for (size_t i = 0; exec[i] != '\0' && j < sizeof(result) - 1; i++) {
if (exec[i] == '%' && exec[i+1] != '\0') {
char code = exec[i+1];
if (code == 'f' || code == 'F' || code == 'u' || code == 'U' ||
code == 'd' || code == 'D' || code == 'n' || code == 'N' ||
code == 'i' || code == 'c' || code == 'k' || code == 'v' ||
code == 'm') {
i++;
if (exec[i+1] == ' ') {
i++;
}
continue;
}
}
result[j++] = exec[i];
}
result[j] = '\0';
while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) {
result[--j] = '\0';
}
size_t result_len = strlen(result);
if (result_len >= 512) {
result_len = 511;
}
memcpy(exec, result, result_len);
exec[result_len] = '\0';
}

View File

@ -66,11 +66,8 @@ Client *client_create(Window window)
} }
} }
int target_width = (work_width * 75) / 100; client->width = orig_width;
int target_height = (work_height * 75) / 100; client->height = orig_height;
client->width = (orig_width > target_width) ? orig_width : target_width;
client->height = (orig_height > target_height) ? orig_height : target_height;
if (client->width > work_width - 20) client->width = work_width - 20; if (client->width > work_width - 20) client->width = work_width - 20;
if (client->height > work_height - 20) client->height = work_height - 20; if (client->height > work_height - 20) client->height = work_height - 20;
@ -603,6 +600,7 @@ void client_set_fullscreen(Client *client, bool fullscreen)
} }
client->flags |= CLIENT_FULLSCREEN; client->flags |= CLIENT_FULLSCREEN;
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, true);
client->x = 0; client->x = 0;
client->y = 0; client->y = 0;
@ -619,8 +617,10 @@ void client_set_fullscreen(Client *client, bool fullscreen)
0, 0, client->width, client->height); 0, 0, client->width, client->height);
XMapWindow(dwn->display, client->window); XMapWindow(dwn->display, client->window);
XRaiseWindow(dwn->display, client->window); XRaiseWindow(dwn->display, client->window);
XSync(dwn->display, False);
} else { } else {
client->flags &= ~CLIENT_FULLSCREEN; client->flags &= ~CLIENT_FULLSCREEN;
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, false);
client->x = client->old_x; client->x = client->old_x;
client->y = client->old_y; client->y = client->old_y;
@ -637,6 +637,9 @@ void client_set_fullscreen(Client *client, bool fullscreen)
XMapWindow(dwn->display, client->frame); XMapWindow(dwn->display, client->frame);
} }
client_configure(client); client_configure(client);
decorations_render(client, true);
client_raise(client);
XSync(dwn->display, False);
} }
} }

View File

@ -75,6 +75,14 @@ void config_set_defaults(Config *cfg)
strncpy(cfg->config_path, "~/.config/dwn/config", sizeof(cfg->config_path) - 1); strncpy(cfg->config_path, "~/.config/dwn/config", sizeof(cfg->config_path) - 1);
strncpy(cfg->log_path, "~/.local/share/dwn/dwn.log", sizeof(cfg->log_path) - 1); strncpy(cfg->log_path, "~/.local/share/dwn/dwn.log", sizeof(cfg->log_path) - 1);
cfg->autostart_enabled = true;
cfg->autostart_xdg = true;
char *autostart_path = expand_path("~/.config/dwn/autostart.d");
if (autostart_path != NULL) {
strncpy(cfg->autostart_path, autostart_path, sizeof(cfg->autostart_path) - 1);
dwn_free(autostart_path);
}
} }
@ -152,6 +160,18 @@ static void handle_config_entry(const char *section, const char *key,
} else if (strcmp(key, "exa_api_key") == 0) { } else if (strcmp(key, "exa_api_key") == 0) {
strncpy(cfg->exa_api_key, value, sizeof(cfg->exa_api_key) - 1); strncpy(cfg->exa_api_key, value, sizeof(cfg->exa_api_key) - 1);
} }
} else if (strcmp(section, "autostart") == 0) {
if (strcmp(key, "enabled") == 0) {
cfg->autostart_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(key, "xdg_autostart") == 0) {
cfg->autostart_xdg = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(key, "path") == 0) {
char *expanded = expand_path(value);
if (expanded != NULL) {
strncpy(cfg->autostart_path, expanded, sizeof(cfg->autostart_path) - 1);
dwn_free(expanded);
}
}
} }
} }

View File

@ -72,6 +72,9 @@ void decorations_render_title_bar(Client *client, bool focused)
int text_x = border + BUTTON_PADDING; int text_x = border + BUTTON_PADDING;
int max_width = client->width - 3 * (BUTTON_SIZE + BUTTON_PADDING) - 2 * BUTTON_PADDING; int max_width = client->width - 3 * (BUTTON_SIZE + BUTTON_PADDING) - 2 * BUTTON_PADDING;
if (max_width < BUTTON_SIZE) {
return;
}
char display_title[256]; char display_title[256];
strncpy(display_title, client->title, sizeof(display_title) - 4); strncpy(display_title, client->title, sizeof(display_title) - 4);
display_title[sizeof(display_title) - 4] = '\0'; display_title[sizeof(display_title) - 4] = '\0';
@ -138,6 +141,11 @@ void decorations_render_buttons(Client *client, bool focused)
return; return;
} }
int min_buttons_width = 3 * (BUTTON_SIZE + BUTTON_PADDING) + BUTTON_PADDING;
if (client->width < min_buttons_width) {
return;
}
Display *dpy = dwn->display; Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors(); const ColorScheme *colors = config_get_colors();
int title_height = config_get_title_height(); int title_height = config_get_title_height();
@ -148,6 +156,10 @@ void decorations_render_buttons(Client *client, bool focused)
int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING; int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING;
int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING; int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING;
if (close_x < border || max_x < border || min_x < border) {
return;
}
unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg; unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg;
XSetForeground(dpy, dwn->gc, bg_color); XSetForeground(dpy, dwn->gc, bg_color);

610
src/demo.c Normal file
View File

@ -0,0 +1,610 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* Demo mode - automated feature showcase
*/
#include "demo.h"
#include "dwn.h"
#include "notifications.h"
#include "workspace.h"
#include "client.h"
#include "layout.h"
#include "keys.h"
#include "ai.h"
#include "news.h"
#include "util.h"
#include "panel.h"
#include "config.h"
#include <string.h>
#define DEMO_STEP_DELAY 3000
typedef struct {
bool active;
DemoPhase phase;
int step;
long phase_start_time;
long step_start_time;
uint32_t current_notification;
Window demo_window;
int demo_workspace_start;
} DemoState;
static DemoState demo = {0};
static void demo_notify(const char *title, const char *body, int timeout)
{
if (demo.current_notification > 0) {
notification_close(demo.current_notification);
}
demo.current_notification = notification_show("DWN Demo", title, body, NULL, timeout);
}
static void demo_next_phase(void)
{
demo.phase++;
demo.step = 0;
demo.phase_start_time = get_time_ms();
demo.step_start_time = demo.phase_start_time;
}
static void demo_next_step(void)
{
demo.step++;
demo.step_start_time = get_time_ms();
}
static long time_in_step(void)
{
return get_time_ms() - demo.step_start_time;
}
static void demo_phase_intro(void)
{
switch (demo.step) {
case 0:
demo_notify("Welcome to DWN",
"DWN is a modern X11 window manager with:\n\n"
"- Tiling, floating, and monocle layouts\n"
"- 9 virtual workspaces\n"
"- AI-powered command palette\n"
"- News ticker with sentiment analysis\n"
"- System tray and notifications\n\n"
"This demo will showcase all features.\n"
"Press Super+Shift+D to stop at any time.", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 5000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_window_mgmt(void)
{
Client *c;
Workspace *ws;
switch (demo.step) {
case 0:
demo_notify("Window Management",
"DWN provides complete window management:\n\n"
"- Alt+F4: Close window\n"
"- Alt+F9: Minimize/restore\n"
"- Alt+F10: Maximize\n"
"- Alt+F11: Fullscreen\n"
"- Alt+Tab: Cycle windows\n\n"
"Let's open a terminal...", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 3000) {
spawn_async("xterm -title 'Demo Terminal' -e 'echo DWN Demo Terminal; sleep 30' &");
demo_next_step();
}
break;
case 2:
if (time_in_step() > 2000) {
demo_notify("Window Created",
"A terminal window was spawned.\n\n"
"Windows can be moved by dragging the title bar\n"
"or resized by dragging edges.\n\n"
"Let's try minimizing...", 0);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 3000) {
ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
demo.demo_window = ws->focused->window;
client_minimize(ws->focused);
demo_notify("Window Minimized",
"The window is now minimized.\n\n"
"Look at the taskbar - minimized windows\n"
"appear in [brackets] with a gray background.\n\n"
"Click it or press Alt+F9 to restore.", 0);
}
demo_next_step();
}
break;
case 4:
if (time_in_step() > 4000) {
c = client_find_by_window(demo.demo_window);
if (c != NULL) {
client_restore(c);
demo_notify("Window Restored",
"The window is restored from the taskbar.\n\n"
"This is how minimize/restore works in DWN.", 0);
}
demo_next_step();
}
break;
case 5:
if (time_in_step() > 3000) {
c = client_find_by_window(demo.demo_window);
if (c != NULL) {
client_close(c);
}
demo_next_phase();
}
break;
}
}
static void demo_phase_workspaces(void)
{
switch (demo.step) {
case 0:
demo.demo_workspace_start = dwn->current_workspace;
demo_notify("Virtual Workspaces",
"DWN provides 9 virtual workspaces:\n\n"
"- F1 to F9: Switch workspace\n"
"- Shift+F1-F9: Move window to workspace\n"
"- Ctrl+Alt+Left/Right: Navigate\n\n"
"Watch the workspace indicators in the panel...", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 3000) {
workspace_switch(1);
demo_notify("Workspace 2", "Switched to workspace 2", 1500);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 2000) {
workspace_switch(2);
demo_notify("Workspace 3", "Switched to workspace 3", 1500);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 2000) {
workspace_switch(3);
demo_notify("Workspace 4", "Switched to workspace 4", 1500);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 2000) {
workspace_switch(demo.demo_workspace_start);
demo_notify("Workspaces Complete",
"Returned to the original workspace.\n\n"
"Each workspace maintains its own:\n"
"- Window layout and positions\n"
"- Focused window\n"
"- Layout mode (tiling/floating/monocle)", 0);
demo_next_step();
}
break;
case 5:
if (time_in_step() > 4000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_layouts(void)
{
Workspace *ws = workspace_get_current();
switch (demo.step) {
case 0:
demo_notify("Layout Modes",
"DWN supports 3 layout modes:\n\n"
"1. Tiling: Windows auto-arrange without overlap\n"
"2. Floating: Free positioning like traditional WMs\n"
"3. Monocle: One window fullscreen at a time\n\n"
"Press Super+Space to cycle layouts.", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_TILING;
workspace_arrange_current();
}
demo_notify("Tiling Layout",
"In tiling mode, windows are automatically\n"
"arranged in a main + stack pattern.\n\n"
"- Super+H/L: Resize main area\n"
"- Super+I/D: Add/remove from main\n\n"
"No overlapping windows - efficient use of space.", 0);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_FLOATING;
workspace_arrange_current();
}
demo_notify("Floating Layout",
"In floating mode, windows can be freely\n"
"positioned and resized anywhere.\n\n"
"This is the traditional desktop behavior\n"
"familiar from Windows and macOS.", 0);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_MONOCLE;
workspace_arrange_current();
}
demo_notify("Monocle Layout",
"In monocle mode, each window takes\n"
"the full screen.\n\n"
"Use Alt+Tab to switch between windows.\n"
"Great for focused, single-task work.", 0);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_TILING;
workspace_arrange_current();
}
demo_next_phase();
}
break;
}
}
static void demo_phase_snapping(void)
{
switch (demo.step) {
case 0:
demo_notify("Window Snapping",
"Quickly arrange windows with snapping:\n\n"
"- Super+Left: Snap to left half (50%)\n"
"- Super+Right: Snap to right half (50%)\n\n"
"Perfect for side-by-side comparisons\n"
"or working with multiple documents.", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 4000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_panels(void)
{
switch (demo.step) {
case 0:
demo_notify("Panel System",
"DWN has two panels with rich functionality:\n\n"
"TOP PANEL (left to right):\n"
"- Workspace indicators (click to switch)\n"
"- Layout mode indicator\n"
"- Taskbar (click to focus/restore)\n"
"- AI status indicator\n"
"- System tray icons\n\n"
"BOTTOM PANEL:\n"
"- Clock with date\n"
"- News ticker", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 5000) {
demo_notify("System Tray",
"The system tray shows:\n\n"
"- Battery level (color-coded)\n"
"- WiFi status (click for network list)\n"
"- Volume (scroll to adjust, click to mute)\n"
"- External app icons (Telegram, etc.)\n\n"
"DWN implements the XEmbed protocol for\n"
"full compatibility with tray applications.", 0);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 5000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_ai(void)
{
switch (demo.step) {
case 0:
if (dwn->ai_enabled) {
demo_notify("AI Integration",
"DWN includes AI-powered features:\n\n"
"COMMAND PALETTE (Super+Shift+A):\n"
"- Ask AI to launch apps: 'open firefox'\n"
"- Get answers: 'what time is it'\n"
"- Execute commands naturally\n\n"
"CONTEXT ANALYSIS (Super+A):\n"
"- See what you're working on\n"
"- Get AI suggestions based on context\n\n"
"EXA SEARCH (Super+Shift+E):\n"
"- Semantic web search\n"
"- Find relevant content by meaning", 0);
} else {
demo_notify("AI Integration",
"DWN includes AI-powered features:\n\n"
"AI is currently DISABLED.\n"
"To enable, set these environment variables:\n\n"
"OPENROUTER_API_KEY=sk-or-v1-your-key\n"
"EXA_API_KEY=your-exa-key\n\n"
"Then restart DWN to enable:\n"
"- AI Command Palette (Super+Shift+A)\n"
"- Context Analysis (Super+A)\n"
"- Exa Semantic Search (Super+Shift+E)", 0);
}
demo_next_step();
break;
case 1:
if (time_in_step() > 6000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_news(void)
{
switch (demo.step) {
case 0:
demo_notify("News Ticker",
"The bottom panel includes a news ticker:\n\n"
"- Headlines scroll automatically\n"
"- Color-coded by sentiment:\n"
" Green = Positive\n"
" Red = Negative\n"
" White = Neutral\n\n"
"CONTROLS:\n"
"- Super+Down: Next article\n"
"- Super+Up: Previous article\n"
"- Super+Return: Open in browser", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 3000) {
news_next_article();
demo_notify("News Navigation", "Moved to next article", 1500);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 2000) {
news_next_article();
demo_notify("News Navigation", "Moved to next article", 1500);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 2000) {
news_prev_article();
demo_notify("News Navigation", "Moved to previous article", 1500);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 2000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_shortcuts(void)
{
switch (demo.step) {
case 0:
demo_notify("Quick Reference",
"ESSENTIAL SHORTCUTS:\n\n"
"Ctrl+Alt+T Open terminal\n"
"Super App launcher\n"
"Alt+F4 Close window\n"
"Alt+Tab Cycle windows\n"
"Super+Space Cycle layouts\n\n"
"WORKSPACES:\n"
"F1-F9 Switch workspace\n"
"Shift+F1-F9 Move window\n\n"
"WINDOW CONTROL:\n"
"Alt+F9 Minimize/restore\n"
"Alt+F10 Maximize\n"
"Alt+F11 Fullscreen\n"
"Super+Left Snap left\n"
"Super+Right Snap right\n\n"
"Press Super+S anytime to see all shortcuts.", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 8000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_complete(void)
{
switch (demo.step) {
case 0:
demo_notify("Demo Complete",
"Thank you for watching the DWN demo!\n\n"
"NEXT STEPS:\n"
"- Super+T: Start interactive tutorial\n"
"- Super+S: View all shortcuts\n"
"- Super+Shift+D: Run this demo again\n\n"
"CONFIGURATION:\n"
"Edit ~/.config/dwn/config to customize\n"
"colors, fonts, keybindings, and more.\n\n"
"Enjoy using DWN!", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 6000) {
notification_close(demo.current_notification);
demo.current_notification = 0;
demo.active = false;
demo.phase = DEMO_IDLE;
}
break;
}
}
void demo_init(void)
{
memset(&demo, 0, sizeof(demo));
demo.phase = DEMO_IDLE;
}
void demo_cleanup(void)
{
if (demo.current_notification > 0) {
notification_close(demo.current_notification);
}
memset(&demo, 0, sizeof(demo));
}
void demo_start(void)
{
if (demo.active) {
demo_stop();
return;
}
if (tutorial_is_active()) {
demo_notify("Demo Unavailable",
"Cannot start demo while tutorial is running.\n"
"Please complete or stop the tutorial first.", 3000);
return;
}
demo.active = true;
demo.phase = DEMO_INTRO;
demo.step = 0;
demo.phase_start_time = get_time_ms();
demo.step_start_time = demo.phase_start_time;
demo.demo_workspace_start = dwn->current_workspace;
}
void demo_stop(void)
{
if (!demo.active) {
return;
}
if (demo.current_notification > 0) {
notification_close(demo.current_notification);
demo.current_notification = 0;
}
demo_notify("Demo Stopped", "Demo mode has been stopped.", 2000);
demo.active = false;
demo.phase = DEMO_IDLE;
demo.step = 0;
}
void demo_update(void)
{
if (!demo.active) {
return;
}
switch (demo.phase) {
case DEMO_IDLE:
break;
case DEMO_INTRO:
demo_phase_intro();
break;
case DEMO_WINDOW_MGMT:
demo_phase_window_mgmt();
break;
case DEMO_WORKSPACES:
demo_phase_workspaces();
break;
case DEMO_LAYOUTS:
demo_phase_layouts();
break;
case DEMO_SNAPPING:
demo_phase_snapping();
break;
case DEMO_PANELS:
demo_phase_panels();
break;
case DEMO_AI:
demo_phase_ai();
break;
case DEMO_NEWS:
demo_phase_news();
break;
case DEMO_SHORTCUTS:
demo_phase_shortcuts();
break;
case DEMO_COMPLETE:
demo_phase_complete();
break;
}
}
bool demo_is_active(void)
{
return demo.active;
}
DemoPhase demo_get_phase(void)
{
return demo.phase;
}
int demo_get_step(void)
{
return demo.step;
}
const char *demo_get_phase_name(DemoPhase phase)
{
switch (phase) {
case DEMO_IDLE: return "Idle";
case DEMO_INTRO: return "Introduction";
case DEMO_WINDOW_MGMT: return "Window Management";
case DEMO_WORKSPACES: return "Workspaces";
case DEMO_LAYOUTS: return "Layouts";
case DEMO_SNAPPING: return "Snapping";
case DEMO_PANELS: return "Panels";
case DEMO_AI: return "AI Features";
case DEMO_NEWS: return "News Ticker";
case DEMO_SHORTCUTS: return "Shortcuts";
case DEMO_COMPLETE: return "Complete";
default: return "Unknown";
}
}

View File

@ -13,6 +13,8 @@
#include "notifications.h" #include "notifications.h"
#include "news.h" #include "news.h"
#include "applauncher.h" #include "applauncher.h"
#include "decorations.h"
#include "demo.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@ -470,6 +472,8 @@ void keys_setup_defaults(void)
keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window"); keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window");
keys_bind(MOD_ALT, XK_F9, key_toggle_minimize, "Toggle minimize");
keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize"); keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize");
keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen"); keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen");
@ -512,11 +516,16 @@ void keys_setup_defaults(void)
keys_bind(MOD_SUPER, XK_i, key_increase_master_count, "Increase master count"); keys_bind(MOD_SUPER, XK_i, key_increase_master_count, "Increase master count");
keys_bind(MOD_SUPER, XK_d, key_decrease_master_count, "Decrease master count"); keys_bind(MOD_SUPER, XK_d, key_decrease_master_count, "Decrease master count");
keys_bind(MOD_SUPER, XK_Left, key_snap_left, "Snap window left");
keys_bind(MOD_SUPER, XK_Right, key_snap_right, "Snap window right");
keys_bind(MOD_SUPER, XK_a, key_toggle_ai, "Toggle AI context"); keys_bind(MOD_SUPER, XK_a, key_toggle_ai, "Toggle AI context");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_ai_command, "AI command"); keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_ai_command, "AI command");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search"); keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_d, key_start_demo, "Start/stop demo mode");
keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts"); keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts");
keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial"); keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial");
@ -603,6 +612,20 @@ void key_toggle_maximize(void)
} }
} }
void key_toggle_minimize(void)
{
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
if (client_is_minimized(ws->focused)) {
client_restore(ws->focused);
} else {
client_minimize(ws->focused);
}
}
void key_focus_next(void) void key_focus_next(void)
{ {
workspace_focus_next(); workspace_focus_next();
@ -762,6 +785,7 @@ void key_show_shortcuts(void)
"Alt+F4 Close window\n" "Alt+F4 Close window\n"
"Alt+Tab Next window\n" "Alt+Tab Next window\n"
"Alt+Shift+Tab Previous window\n" "Alt+Shift+Tab Previous window\n"
"Alt+F9 Toggle minimize\n"
"Alt+F10 Toggle maximize\n" "Alt+F10 Toggle maximize\n"
"Alt+F11 Toggle fullscreen\n" "Alt+F11 Toggle fullscreen\n"
"Super+F9 Toggle floating\n" "Super+F9 Toggle floating\n"
@ -778,6 +802,8 @@ void key_show_shortcuts(void)
"Super+L Expand master area\n" "Super+L Expand master area\n"
"Super+I Add to master\n" "Super+I Add to master\n"
"Super+D Remove from master\n" "Super+D Remove from master\n"
"Super+Left Snap window left (50%)\n"
"Super+Right Snap window right (50%)\n"
"\n" "\n"
"=== AI Features ===\n" "=== AI Features ===\n"
"Super+A Show AI context\n" "Super+A Show AI context\n"
@ -787,6 +813,7 @@ void key_show_shortcuts(void)
"=== Help & System ===\n" "=== Help & System ===\n"
"Super+S Show shortcuts (this)\n" "Super+S Show shortcuts (this)\n"
"Super+T Interactive tutorial\n" "Super+T Interactive tutorial\n"
"Super+Shift+D Demo mode\n"
"Super+Backspace Quit DWN"; "Super+Backspace Quit DWN";
notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0); notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0);
@ -821,3 +848,98 @@ void key_screenshot(void)
{ {
spawn_async("xfce4-screenshooter"); spawn_async("xfce4-screenshooter");
} }
void key_snap_left(void)
{
if (dwn == NULL) {
return;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
Client *c = ws->focused;
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
int work_height = dwn->screen_height;
if (dwn->config != NULL) {
if (dwn->config->top_panel_enabled) {
work_y += config_get_panel_height();
work_height -= config_get_panel_height();
}
if (dwn->config->bottom_panel_enabled) {
work_height -= config_get_panel_height();
}
}
int gap = config_get_gap();
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
c->x = work_x + gap;
c->y = work_y + gap;
c->width = (work_width / 2) - (gap * 2);
c->height = work_height - (gap * 2);
client_configure(c);
decorations_render(c, true);
}
void key_snap_right(void)
{
if (dwn == NULL) {
return;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
Client *c = ws->focused;
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
int work_height = dwn->screen_height;
if (dwn->config != NULL) {
if (dwn->config->top_panel_enabled) {
work_y += config_get_panel_height();
work_height -= config_get_panel_height();
}
if (dwn->config->bottom_panel_enabled) {
work_height -= config_get_panel_height();
}
}
int gap = config_get_gap();
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
c->x = work_x + (work_width / 2) + gap;
c->y = work_y + gap;
c->width = (work_width / 2) - (gap * 2);
c->height = work_height - (gap * 2);
client_configure(c);
decorations_render(c, true);
}
void key_start_demo(void)
{
if (demo_is_active()) {
demo_stop();
} else {
demo_start();
}
}

View File

@ -18,6 +18,8 @@
#include "news.h" #include "news.h"
#include "applauncher.h" #include "applauncher.h"
#include "ai.h" #include "ai.h"
#include "autostart.h"
#include "demo.h"
#include "util.h" #include "util.h"
#include <stdio.h> #include <stdio.h>
@ -251,6 +253,11 @@ static void handle_destroy_notify(XDestroyWindowEvent *ev)
return; return;
} }
if (xembed_find_icon(ev->window) != NULL) {
xembed_remove_icon(ev->window);
return;
}
Client *c = client_find_by_window(ev->window); Client *c = client_find_by_window(ev->window);
if (c != NULL) { if (c != NULL) {
client_unmanage(c); client_unmanage(c);
@ -295,6 +302,13 @@ static void handle_property_notify(XPropertyEvent *ev)
return; return;
} }
if (ev->atom == ewmh.XEMBED_INFO) {
if (xembed_find_icon(ev->window) != NULL) {
xembed_update_icon_state(ev->window);
return;
}
}
Client *c = client_find_by_window(ev->window); Client *c = client_find_by_window(ev->window);
if (c == NULL) { if (c == NULL) {
return; return;
@ -560,6 +574,12 @@ static void handle_client_message(XClientMessageEvent *ev)
if (desktop >= 0 && desktop < MAX_WORKSPACES) { if (desktop >= 0 && desktop < MAX_WORKSPACES) {
workspace_switch(desktop); workspace_switch(desktop);
} }
} else if (ev->message_type == ewmh.NET_SYSTEM_TRAY_OPCODE) {
long opcode = ev->data.l[1];
if (opcode == SYSTEM_TRAY_REQUEST_DOCK) {
Window icon_win = ev->data.l[2];
xembed_dock_icon(icon_win);
}
} }
} }
@ -708,16 +728,23 @@ int dwn_init(void)
systray_init(); systray_init();
xembed_init();
news_init(); news_init();
applauncher_init(); applauncher_init();
autostart_init();
autostart_run();
keys_init(); keys_init();
notifications_init(); notifications_init();
ai_init(); ai_init();
demo_init();
atoms_setup_ewmh(); atoms_setup_ewmh();
XSelectInput(dwn->display, dwn->root, XSelectInput(dwn->display, dwn->root,
@ -745,11 +772,14 @@ void dwn_cleanup(void)
{ {
LOG_INFO("DWN shutting down"); LOG_INFO("DWN shutting down");
demo_cleanup();
ai_cleanup(); ai_cleanup();
notifications_cleanup(); notifications_cleanup();
news_cleanup(); news_cleanup();
applauncher_cleanup(); applauncher_cleanup();
autostart_cleanup();
keys_cleanup(); keys_cleanup();
xembed_cleanup();
systray_cleanup(); systray_cleanup();
panels_cleanup(); panels_cleanup();
decorations_cleanup(); decorations_cleanup();
@ -806,6 +836,8 @@ void dwn_run(void)
exa_process_pending(); exa_process_pending();
demo_update();
notifications_update(); notifications_update();
long now = get_time_ms(); long now = get_time_ms();

View File

@ -17,6 +17,7 @@
#include <unistd.h> #include <unistd.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <pthread.h> #include <pthread.h>
#include <stdatomic.h>
#include <curl/curl.h> #include <curl/curl.h>
#include <X11/Xft/Xft.h> #include <X11/Xft/Xft.h>
@ -41,7 +42,7 @@ NewsState news_state = {0};
static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t fetch_thread; static pthread_t fetch_thread;
static volatile int fetch_running = 0; static atomic_int fetch_running = 0;
typedef struct { typedef struct {
char *data; char *data;
@ -428,9 +429,9 @@ void news_init(void)
void news_cleanup(void) void news_cleanup(void)
{ {
if (fetch_running) { if (atomic_load(&fetch_running)) {
pthread_join(fetch_thread, NULL); pthread_join(fetch_thread, NULL);
fetch_running = 0; atomic_store(&fetch_running, 0);
} }
} }
@ -444,17 +445,17 @@ void news_fetch_async(void)
news_state.fetching = true; news_state.fetching = true;
pthread_mutex_unlock(&news_mutex); pthread_mutex_unlock(&news_mutex);
if (fetch_running) { if (atomic_load(&fetch_running)) {
pthread_join(fetch_thread, NULL); pthread_join(fetch_thread, NULL);
} }
fetch_running = 1; atomic_store(&fetch_running, 1);
if (pthread_create(&fetch_thread, NULL, news_fetch_thread, NULL) != 0) { if (pthread_create(&fetch_thread, NULL, news_fetch_thread, NULL) != 0) {
pthread_mutex_lock(&news_mutex); pthread_mutex_lock(&news_mutex);
news_state.fetching = false; news_state.fetching = false;
news_state.has_error = true; news_state.has_error = true;
pthread_mutex_unlock(&news_mutex); pthread_mutex_unlock(&news_mutex);
fetch_running = 0; atomic_store(&fetch_running, 0);
} }
} }
@ -578,6 +579,15 @@ void news_render(Panel *panel, int x, int max_width, int *used_width)
return; return;
} }
int text_y;
if (dwn->xft_font != NULL) {
text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2;
} else {
text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2;
}
const ColorScheme *colors = config_get_colors();
pthread_mutex_lock(&news_mutex); pthread_mutex_lock(&news_mutex);
if (news_state.article_count == 0) { if (news_state.article_count == 0) {
@ -603,21 +613,8 @@ void news_render(Panel *panel, int x, int max_width, int *used_width)
int separator_width = news_text_width(NEWS_SEPARATOR); int separator_width = news_text_width(NEWS_SEPARATOR);
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width; int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
pthread_mutex_unlock(&news_mutex);
const ColorScheme *colors = config_get_colors();
int text_y;
if (dwn->xft_font != NULL) {
text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2;
} else {
text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2;
}
XRectangle clip_rect = { x, 0, max_width, panel->height }; XRectangle clip_rect = { x, 0, max_width, panel->height };
pthread_mutex_lock(&news_mutex);
int draw_x = x - scroll_offset; int draw_x = x - scroll_offset;
int articles_drawn = 0; int articles_drawn = 0;
int start_article = 0; int start_article = 0;

View File

@ -10,8 +10,21 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <X11/Xft/Xft.h> #include <X11/Xft/Xft.h>
static sigjmp_buf dbus_timeout_jmp;
static volatile sig_atomic_t dbus_timeout_occurred = 0;
static void dbus_timeout_handler(int sig)
{
(void)sig;
dbus_timeout_occurred = 1;
siglongjmp(dbus_timeout_jmp, 1);
}
DBusConnection *dbus_conn = NULL; DBusConnection *dbus_conn = NULL;
static Notification *notification_list = NULL; static Notification *notification_list = NULL;
@ -124,7 +137,27 @@ bool notifications_init(void)
DBusError err; DBusError err;
dbus_error_init(&err); dbus_error_init(&err);
dbus_timeout_occurred = 0;
struct sigaction sa, old_sa;
sa.sa_handler = dbus_timeout_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, &old_sa);
if (sigsetjmp(dbus_timeout_jmp, 1) != 0) {
sigaction(SIGALRM, &old_sa, NULL);
alarm(0);
LOG_WARN("D-Bus connection timed out (5s) - notifications disabled");
return false;
}
alarm(5);
dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err); dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
alarm(0);
sigaction(SIGALRM, &old_sa, NULL);
if (dbus_error_is_set(&err)) { if (dbus_error_is_set(&err)) {
LOG_ERROR("D-Bus connection error: %s", err.message); LOG_ERROR("D-Bus connection error: %s", err.message);
dbus_error_free(&err); dbus_error_free(&err);
@ -731,10 +764,19 @@ void notifications_position(void)
y += config_get_panel_height(); y += config_get_panel_height();
} }
int max_y = dwn->screen_height - NOTIFICATION_MARGIN;
for (Notification *n = notification_list; n != NULL; n = n->next) { for (Notification *n = notification_list; n != NULL; n = n->next) {
if (y + n->height > max_y) {
if (n->window != None) {
XUnmapWindow(dwn->display, n->window);
}
continue;
}
if (n->window != None) { if (n->window != None) {
int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN; int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN;
XMoveWindow(dwn->display, n->window, x, y); XMoveWindow(dwn->display, n->window, x, y);
XMapRaised(dwn->display, n->window);
} }
y += n->height + NOTIFICATION_MARGIN; y += n->height + NOTIFICATION_MARGIN;
} }

View File

@ -346,11 +346,15 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
int text_y = panel_text_y(panel->height); int text_y = panel_text_y(panel->height);
int current_x = x; int current_x = x;
int available_width = panel->width - x - 100 - PANEL_PADDING; int systray_actual_width = systray_get_width();
int ai_width = dwn->ai_enabled ? 60 : 0;
int right_reserve = systray_actual_width + ai_width + PANEL_PADDING * 2;
int available_width = panel->width - x - right_reserve;
if (available_width < 0) available_width = 0;
int item_count = 0; int item_count = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) { for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { if (c->workspace == (unsigned int)dwn->current_workspace) {
item_count++; item_count++;
} }
} }
@ -368,20 +372,32 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
Workspace *ws = workspace_get_current(); Workspace *ws = workspace_get_current();
for (Client *c = dwn->client_list; c != NULL; c = c->next) { for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace != (unsigned int)dwn->current_workspace || client_is_minimized(c)) { if (c->workspace != (unsigned int)dwn->current_workspace) {
continue; continue;
} }
bool focused = (ws != NULL && ws->focused == c); bool focused = (ws != NULL && ws->focused == c);
bool minimized = client_is_minimized(c);
unsigned long bg = focused ? colors->workspace_active : colors->panel_bg; unsigned long bg;
if (minimized) {
bg = colors->workspace_inactive;
} else if (focused) {
bg = colors->workspace_active;
} else {
bg = colors->panel_bg;
}
XSetForeground(dpy, dwn->gc, bg); XSetForeground(dpy, dwn->gc, bg);
XFillRectangle(dpy, panel->buffer, dwn->gc, XFillRectangle(dpy, panel->buffer, dwn->gc,
current_x, 2, item_width - 2, panel->height - 4); current_x, 2, item_width - 2, panel->height - 4);
char title[64]; char title[64];
strncpy(title, c->title, sizeof(title) - 4); if (minimized) {
title[sizeof(title) - 4] = '\0'; snprintf(title, sizeof(title), "[%s]", c->title);
} else {
strncpy(title, c->title, sizeof(title) - 1);
title[sizeof(title) - 1] = '\0';
}
int max_text_width = item_width - 8; int max_text_width = item_width - 8;
bool truncated = false; bool truncated = false;
@ -405,7 +421,14 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
strncat(title, "...", sizeof(title) - strlen(title) - 1); strncat(title, "...", sizeof(title) - strlen(title) - 1);
} }
unsigned long fg = focused ? colors->panel_bg : colors->panel_fg; unsigned long fg;
if (minimized) {
fg = colors->panel_fg;
} else if (focused) {
fg = colors->panel_bg;
} else {
fg = colors->panel_fg;
}
panel_draw_text(panel->buffer, current_x + 4, text_y, title, strlen(title), fg); panel_draw_text(panel->buffer, current_x + 4, text_y, title, strlen(title), fg);
current_x += item_width; current_x += item_width;
@ -506,7 +529,11 @@ void panel_handle_click(Panel *panel, int x, int y, int button)
Client *c = panel_hit_test_taskbar(panel, x, y); Client *c = panel_hit_test_taskbar(panel, x, y);
if (c != NULL) { if (c != NULL) {
if (button == 1) { if (button == 1) {
client_focus(c); if (client_is_minimized(c)) {
client_restore(c);
} else {
client_focus(c);
}
} else if (button == 3) { } else if (button == 3) {
client_close(c); client_close(c);
} }
@ -542,16 +569,23 @@ Client *panel_hit_test_taskbar(Panel *panel, int x, int y)
return NULL; return NULL;
} }
int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + 50; Workspace *ws = workspace_get_current();
const char *layout_symbol = layout_get_symbol(ws != NULL ? ws->layout : LAYOUT_TILING);
int layout_width = panel_text_width(layout_symbol, strlen(layout_symbol));
int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + layout_width + WIDGET_SPACING;
if (x < taskbar_start) { if (x < taskbar_start) {
return NULL; return NULL;
} }
int available_width = panel->width - taskbar_start - 100 - PANEL_PADDING; int systray_actual_width = systray_get_width();
int ai_width = dwn->ai_enabled ? 60 : 0;
int right_reserve = systray_actual_width + ai_width + PANEL_PADDING * 2;
int available_width = panel->width - taskbar_start - right_reserve;
if (available_width < 0) available_width = 0;
int item_count = 0; int item_count = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) { for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { if (c->workspace == (unsigned int)dwn->current_workspace) {
item_count++; item_count++;
} }
} }
@ -569,7 +603,7 @@ Client *panel_hit_test_taskbar(Panel *panel, int x, int y)
int i = 0; int i = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) { for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { if (c->workspace == (unsigned int)dwn->current_workspace) {
if (i == index) { if (i == index) {
return c; return c;
} }
@ -615,6 +649,19 @@ void panel_toggle(Panel *panel)
} }
} }
void panel_raise_all(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
if (dwn->top_panel != NULL && dwn->top_panel->visible) {
XRaiseWindow(dwn->display, dwn->top_panel->window);
}
if (dwn->bottom_panel != NULL && dwn->bottom_panel->visible) {
XRaiseWindow(dwn->display, dwn->bottom_panel->window);
}
}
void panel_update_clock(void) void panel_update_clock(void)
{ {

View File

@ -9,6 +9,7 @@
#include "config.h" #include "config.h"
#include "util.h" #include "util.h"
#include "notifications.h" #include "notifications.h"
#include "atoms.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -50,6 +51,10 @@ BatteryState battery_state = {0};
DropdownMenu *wifi_menu = NULL; DropdownMenu *wifi_menu = NULL;
VolumeSlider *volume_slider = NULL; VolumeSlider *volume_slider = NULL;
TrayIcon tray_icons[MAX_TRAY_ICONS];
int tray_icon_count = 0;
Window tray_selection_owner = None;
static pthread_t update_thread; static pthread_t update_thread;
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
static volatile int thread_running = 0; static volatile int thread_running = 0;
@ -58,6 +63,7 @@ static int systray_x = 0;
static int systray_width = 0; static int systray_width = 0;
static int wifi_icon_x = 0; static int wifi_icon_x = 0;
static int audio_icon_x = 0; static int audio_icon_x = 0;
static int xembed_icons_x = 0;
static void wifi_update_state_internal(void); static void wifi_update_state_internal(void);
static void audio_update_state_internal(void); static void audio_update_state_internal(void);
@ -854,7 +860,13 @@ int systray_get_width(void)
snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str);
} }
return get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label); int builtin_width = get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label);
int xembed_width = xembed_get_icons_width();
if (xembed_width > 0) {
return xembed_width + SYSTRAY_SPACING + builtin_width;
}
return builtin_width;
} }
void systray_render(Panel *panel, int x, int *width) void systray_render(Panel *panel, int x, int *width)
@ -885,6 +897,12 @@ void systray_render(Panel *panel, int x, int *width)
systray_x = x; systray_x = x;
int current_x = x; int current_x = x;
int xembed_width = xembed_get_icons_width();
if (xembed_width > 0) {
xembed_render_icons(panel, current_x);
current_x += xembed_width + SYSTRAY_SPACING;
}
audio_icon_x = current_x; audio_icon_x = current_x;
char audio_label[32]; char audio_label[32];
const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH; const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH;
@ -915,6 +933,10 @@ void systray_render(Panel *panel, int x, int *width)
int systray_hit_test(int x) int systray_hit_test(int x)
{ {
int xembed_idx = xembed_hit_test(x);
if (xembed_idx >= 0) {
return 2;
}
if (x >= wifi_icon_x) { if (x >= wifi_icon_x) {
return 0; return 0;
} }
@ -928,6 +950,11 @@ void systray_handle_click(int x, int y, int button)
{ {
int widget = systray_hit_test(x); int widget = systray_hit_test(x);
if (widget == 2) {
xembed_handle_click(x, y, button);
return;
}
if (widget == 0) { if (widget == 0) {
if (volume_slider != NULL && volume_slider->visible) { if (volume_slider != NULL && volume_slider->visible) {
volume_slider_hide(volume_slider); volume_slider_hide(volume_slider);
@ -1034,3 +1061,365 @@ AudioState systray_get_audio_snapshot(void)
pthread_mutex_unlock(&state_mutex); pthread_mutex_unlock(&state_mutex);
return snapshot; return snapshot;
} }
void xembed_init(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
memset(tray_icons, 0, sizeof(tray_icons));
tray_icon_count = 0;
tray_selection_owner = XCreateSimpleWindow(dpy, dwn->root, -1, -1, 1, 1, 0, 0, 0);
if (tray_selection_owner == None) {
LOG_WARN("Failed to create tray selection owner window");
return;
}
Window existing = XGetSelectionOwner(dpy, ewmh.NET_SYSTEM_TRAY_S0);
if (existing != None) {
LOG_WARN("Another system tray is already running");
XDestroyWindow(dpy, tray_selection_owner);
tray_selection_owner = None;
return;
}
XSetSelectionOwner(dpy, ewmh.NET_SYSTEM_TRAY_S0, tray_selection_owner, CurrentTime);
if (XGetSelectionOwner(dpy, ewmh.NET_SYSTEM_TRAY_S0) != tray_selection_owner) {
LOG_WARN("Failed to acquire system tray selection");
XDestroyWindow(dpy, tray_selection_owner);
tray_selection_owner = None;
return;
}
long orientation = 0;
XChangeProperty(dpy, tray_selection_owner, ewmh.NET_SYSTEM_TRAY_ORIENTATION,
XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&orientation, 1);
VisualID visual_id = XVisualIDFromVisual(DefaultVisual(dpy, dwn->screen));
XChangeProperty(dpy, tray_selection_owner, ewmh.NET_SYSTEM_TRAY_VISUAL,
XA_VISUALID, 32, PropModeReplace,
(unsigned char *)&visual_id, 1);
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.xclient.type = ClientMessage;
ev.xclient.window = dwn->root;
ev.xclient.message_type = ewmh.MANAGER;
ev.xclient.format = 32;
ev.xclient.data.l[0] = CurrentTime;
ev.xclient.data.l[1] = ewmh.NET_SYSTEM_TRAY_S0;
ev.xclient.data.l[2] = tray_selection_owner;
ev.xclient.data.l[3] = 0;
ev.xclient.data.l[4] = 0;
XSendEvent(dpy, dwn->root, False, StructureNotifyMask, &ev);
XFlush(dpy);
LOG_INFO("XEmbed system tray initialized");
}
void xembed_cleanup(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window != None) {
XReparentWindow(dwn->display, tray_icons[i].window, dwn->root, 0, 0);
}
}
tray_icon_count = 0;
if (tray_selection_owner != None) {
XDestroyWindow(dwn->display, tray_selection_owner);
tray_selection_owner = None;
}
LOG_DEBUG("XEmbed system tray cleaned up");
}
void xembed_send_message(Window icon, long message, long detail, long data1, long data2)
{
if (dwn == NULL || dwn->display == NULL || icon == None) {
return;
}
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.xclient.type = ClientMessage;
ev.xclient.window = icon;
ev.xclient.message_type = ewmh.XEMBED;
ev.xclient.format = 32;
ev.xclient.data.l[0] = CurrentTime;
ev.xclient.data.l[1] = message;
ev.xclient.data.l[2] = detail;
ev.xclient.data.l[3] = data1;
ev.xclient.data.l[4] = data2;
XSendEvent(dwn->display, icon, False, NoEventMask, &ev);
}
static bool xembed_get_info(Window icon, unsigned long *version, unsigned long *flags)
{
if (dwn == NULL || dwn->display == NULL || icon == None) {
return false;
}
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
if (XGetWindowProperty(dwn->display, icon, ewmh.XEMBED_INFO,
0, 2, False, ewmh.XEMBED_INFO,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) != Success || data == NULL) {
return false;
}
if (nitems >= 2 && actual_format == 32) {
unsigned long *info = (unsigned long *)data;
if (version != NULL) *version = info[0];
if (flags != NULL) *flags = info[1];
XFree(data);
return true;
}
XFree(data);
return false;
}
bool xembed_dock_icon(Window icon_win)
{
if (dwn == NULL || dwn->display == NULL || dwn->top_panel == NULL) {
return false;
}
if (icon_win == None) {
return false;
}
if (tray_icon_count >= MAX_TRAY_ICONS) {
LOG_WARN("Maximum tray icons reached");
return false;
}
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window == icon_win) {
return true;
}
}
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, icon_win, &wa)) {
LOG_WARN("Failed to get tray icon attributes");
return false;
}
Display *dpy = dwn->display;
XSelectInput(dpy, icon_win, StructureNotifyMask | PropertyChangeMask);
XReparentWindow(dpy, icon_win, dwn->top_panel->window, 0, 0);
XResizeWindow(dpy, icon_win, TRAY_ICON_SIZE, TRAY_ICON_SIZE);
unsigned long version = 0, flags = 0;
bool has_info = xembed_get_info(icon_win, &version, &flags);
TrayIcon *icon = &tray_icons[tray_icon_count];
icon->window = icon_win;
icon->width = TRAY_ICON_SIZE;
icon->height = TRAY_ICON_SIZE;
icon->mapped = !has_info || (flags & XEMBED_MAPPED);
icon->x = 0;
tray_icon_count++;
xembed_send_message(icon_win, XEMBED_EMBEDDED_NOTIFY, 0,
dwn->top_panel->window, XEMBED_PROTOCOL_VERSION);
if (icon->mapped) {
XMapWindow(dpy, icon_win);
}
XFlush(dpy);
LOG_INFO("Docked tray icon: window=%lu, mapped=%d", icon_win, icon->mapped);
panel_render_all();
return true;
}
void xembed_remove_icon(Window icon_win)
{
if (icon_win == None) {
return;
}
int found = -1;
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window == icon_win) {
found = i;
break;
}
}
if (found < 0) {
return;
}
LOG_DEBUG("Removing tray icon: window=%lu", icon_win);
for (int i = found; i < tray_icon_count - 1; i++) {
tray_icons[i] = tray_icons[i + 1];
}
tray_icon_count--;
memset(&tray_icons[tray_icon_count], 0, sizeof(TrayIcon));
panel_render_all();
}
TrayIcon *xembed_find_icon(Window icon_win)
{
if (icon_win == None) {
return NULL;
}
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window == icon_win) {
return &tray_icons[i];
}
}
return NULL;
}
int xembed_get_icons_width(void)
{
if (tray_icon_count == 0) {
return 0;
}
return tray_icon_count * (TRAY_ICON_SIZE + TRAY_ICON_SPACING);
}
void xembed_render_icons(Panel *panel, int x)
{
if (panel == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
int current_x = x;
int y = (panel->height - TRAY_ICON_SIZE) / 2;
xembed_icons_x = x;
for (int i = 0; i < tray_icon_count; i++) {
TrayIcon *icon = &tray_icons[i];
if (icon->window == None) {
continue;
}
icon->x = current_x;
if (icon->mapped) {
XMoveResizeWindow(dpy, icon->window, current_x, y,
TRAY_ICON_SIZE, TRAY_ICON_SIZE);
}
current_x += TRAY_ICON_SIZE + TRAY_ICON_SPACING;
}
}
int xembed_hit_test(int x)
{
if (tray_icon_count == 0) {
return -1;
}
for (int i = 0; i < tray_icon_count; i++) {
TrayIcon *icon = &tray_icons[i];
if (x >= icon->x && x < icon->x + TRAY_ICON_SIZE) {
return i;
}
}
return -1;
}
void xembed_handle_click(int x, int y, int button)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
int index = xembed_hit_test(x);
if (index < 0 || index >= tray_icon_count) {
return;
}
TrayIcon *icon = &tray_icons[index];
if (icon->window == None || !icon->mapped) {
return;
}
int icon_x = x - icon->x;
int icon_y = y;
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.xbutton.type = ButtonPress;
ev.xbutton.window = icon->window;
ev.xbutton.root = dwn->root;
ev.xbutton.subwindow = None;
ev.xbutton.time = CurrentTime;
ev.xbutton.x = icon_x;
ev.xbutton.y = icon_y;
ev.xbutton.x_root = x;
ev.xbutton.y_root = y;
ev.xbutton.state = 0;
ev.xbutton.button = button;
ev.xbutton.same_screen = True;
XSendEvent(dwn->display, icon->window, False, ButtonPressMask, &ev);
ev.xbutton.type = ButtonRelease;
XSendEvent(dwn->display, icon->window, False, ButtonReleaseMask, &ev);
XFlush(dwn->display);
}
void xembed_update_icon_state(Window icon_win)
{
TrayIcon *icon = xembed_find_icon(icon_win);
if (icon == NULL) {
return;
}
unsigned long version = 0, flags = 0;
if (!xembed_get_info(icon_win, &version, &flags)) {
return;
}
bool should_map = (flags & XEMBED_MAPPED) != 0;
if (should_map != icon->mapped) {
icon->mapped = should_map;
if (should_map) {
XMapWindow(dwn->display, icon_win);
} else {
XUnmapWindow(dwn->display, icon_win);
}
panel_render_all();
}
}

View File

@ -10,6 +10,7 @@
#include "atoms.h" #include "atoms.h"
#include "util.h" #include "util.h"
#include "config.h" #include "config.h"
#include "panel.h"
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@ -106,6 +107,8 @@ void workspace_switch(int index)
atoms_set_current_desktop(index); atoms_set_current_desktop(index);
panel_raise_all();
XUngrabServer(dwn->display); XUngrabServer(dwn->display);
XSync(dwn->display, False); XSync(dwn->display, False);
@ -324,6 +327,7 @@ void workspace_arrange_current(void)
{ {
XGrabServer(dwn->display); XGrabServer(dwn->display);
workspace_arrange(dwn->current_workspace); workspace_arrange(dwn->current_workspace);
panel_raise_all();
XUngrabServer(dwn->display); XUngrabServer(dwn->display);
XSync(dwn->display, False); XSync(dwn->display, False);
} }