docs: expand README with detailed features, architecture, and configuration guides

This commit is contained in:
retoor 2026-01-27 02:50:47 +01:00
parent a75c1d90cc
commit 2017a9b666
18 changed files with 1216 additions and 336 deletions

471
README.md
View File

@ -2,100 +2,240 @@
retoor <retoor@molodetz.nl>
A production-ready X11 window manager written in ANSI C. DWN implements EWMH and ICCCM protocols for cross-compatibility while providing tiling-first workflow management with integrated desktop components.
A production-ready X11 window manager written in ANSI C with EWMH/ICCCM protocol compliance, tiling-first workflow, integrated desktop components, and optional AI integration via OpenRouter API.
## Architecture
## Design Philosophy
DWN uses a modular architecture with specialized subsystems for window management, layout algorithms, panel rendering, system tray, notifications, and optional AI integration. All state is managed through a global singleton with strict encapsulation patterns.
DWN prioritizes a seamless, distraction-free desktop experience:
The window manager operates with zero window borders and zero gaps between tiles, providing a seamless, professional appearance. Title bars remain at 28px height for window controls.
- **Zero borders and gaps**: Windows tile edge-to-edge without visual separation
- **Minimal chrome**: 28px title bars provide window controls without excess decoration
- **Tiling-first**: Master-stack layout as the default, with floating and monocle modes available
- **Keyboard-driven**: Comprehensive shortcuts for all operations
- **Modular architecture**: Single-responsibility modules with strict encapsulation
- **ANSI C**: Maximum portability and minimal dependencies
## Core Features
## Features
### Layout Management
- **Tiling Mode**: Master-stack layout with configurable ratios
- **Floating Mode**: Traditional overlapping windows
- **Monocle Mode**: Single fullscreen window
- **Composable Snapping**: Quarter-screen, half-screen, and full-screen snapping via Super+Arrow keys
### Window Management
**Layout Modes**
- **Tiling**: Master-stack layout with configurable master ratio (0.1-0.9) and master window count
- **Floating**: Traditional overlapping windows with free positioning
- **Monocle**: Single maximized window per workspace
**Composable Window Snapping**
- Super+Arrow keys snap windows to half-screen positions
- Double-press extends to full width/height
- Combinations create quarter-screen positions (e.g., Super+Left then Super+Up = top-left quarter)
**Directional Resizing**
- Resize windows from any edge or corner
- Respects layout bounds and snap constraints
**Window States**
- Fullscreen (Alt+F11): Covers entire screen including panels
- Maximized (Alt+F10): Fills usable area with title bar
- Minimized (Alt+F9): Hidden from view
- Floating (Super+F9): Exempt from tiling layout
- Sticky: Visible on all workspaces
- Urgent: Demands attention with taskbar highlight
### Workspace System
- 9 independent virtual workspaces (F1-F9)
- Per-workspace layout persistence
- Workspace indicators in panel
- Fast workspace switching with Shift+F1-F9 to move windows
- MRU (Most Recently Used) focus stack per workspace
- Alt-Tab cycles through MRU order
- Shift+F1-F9 moves focused window to target workspace
- Ctrl+Alt+Left/Right navigates adjacent workspaces
### Integrated Components
- **Top Panel**: Workspace indicators, taskbar, system tray, clock
- **Bottom Panel**: Additional status information
- **System Tray**: XEmbed protocol implementation supporting external applications
- **System Widgets**: Battery indicator, volume control, WiFi manager
- **Notification Daemon**: D-Bus org.freedesktop.Notifications implementation
### Panels
**Top Panel**
- Workspace indicators with active/inactive/urgent states
- Taskbar with window buttons
- System tray with XEmbed protocol support
- Battery indicator (multi-battery support)
- Volume control with slider
- Top memory process display
- Top CPU process display
- Key press counter
- Mouse distance traveled
- Clock
**Bottom Panel**
- News ticker with scrolling headlines
- Configurable visibility
### System Tray
**XEmbed Protocol**
- Acquires `_NET_SYSTEM_TRAY_S0` selection
- Docks external application icons (nm-applet, blueman, Telegram, etc.)
- Forwards click events to icon windows
- Automatic cleanup on application close
**Built-in Widgets**
- Battery: Percentage display, charging indicator, multi-battery support
- Volume: Click for slider, scroll to adjust, right-click to mute
- Process monitors: Rotating display of top CPU/memory consumers
### Ambient Glow Effects
Visual feedback system with phase-offset animations:
- Panel background subtle color cycling
- Workspace indicator glow
- Taskbar button highlighting
- Clock and statistics display
- Window title text glow on focus
- Configurable animation speed
### Activity Tracking
- **Key press counter**: Total keypresses displayed in panel
- **Mouse distance**: Cumulative mouse movement in pixels
- **XInput2 integration**: Raw event monitoring for accurate tracking
### Focus Transitions
- Animated color transitions on window focus changes
- Taskbar button color interpolation
- Title bar glow animations
- Bold font rendering for Alt-Tab selection
### Notification System
D-Bus implementation of `org.freedesktop.Notifications`:
- Receives notifications from all applications
- Stacked display with timeout management
- Urgency levels: low, normal, critical
- Click to dismiss
### News Ticker
- Real-time news feed in panel
- Scrolling article titles
- Super+Return to open current article in browser
- Configurable news sources
- Fetches headlines from configured sources
- Smooth pixel-based scrolling animation
- Super+Return opens current article in browser
- Automatic refresh interval
- Sentiment indicators (positive/negative/neutral)
### AI Integration
- **Command Palette** (Super+Shift+A): Natural language window management
- **Context Analysis** (Super+A): Workspace and task detection
- **Exa Semantic Search** (Super+Shift+E): Intelligent web search
- Powered by OpenRouter API (supports multiple LLM providers)
### Interactive Features
- **Tutorial System** (Super+T): Step-by-step keyboard shortcut training
- **Demo Mode** (Super+Shift+D): Automated feature showcase
- **Keyboard Shortcuts Help** (Super+S): Quick reference overlay
**Command Palette (Super+Shift+A)**
- Natural language command execution
- OpenRouter API with configurable model selection
- Context-aware responses based on current workspace
**Context Analysis (Super+A)**
- Task type detection (coding, browsing, communication)
- Window and application analysis
- Intelligent suggestions
**Exa Semantic Search (Super+Shift+E)**
- Meaning-based web search
- Results displayed in dmenu/rofi
- Select to open in browser
### Screenshot and OCR
**Screenshot API**
- Fullscreen capture
- Active window capture
- Area selection capture
- Async capture with callbacks
- Base64 encoding for API transmission
- PNG output
**OCR API**
- Tesseract-based text extraction
- Multi-language support
- Confidence scoring
- Async processing
### WebSocket API
Programmatic control on port 8777:
| Command | Description |
|---------|-------------|
| `get_status` | Window manager state |
| `get_workspaces` | Workspace information |
| `get_clients` | All managed windows |
| `switch_workspace` | Change active workspace |
| `focus_client` | Focus specific window |
| `run_command` | Execute shell command |
| `screenshot` | Capture screen |
| `ocr` | Extract text from image |
### Automation
- **XDG Autostart**: Automatic application launching from .desktop files
- **Service Manager**: Background process management
- **WebSocket API**: Remote control and state monitoring on port 8777
**XDG Autostart**
- Scans `/etc/xdg/autostart/` and `~/.config/autostart/`
- Parses `.desktop` files
- Custom autostart directory support
**Service Manager**
- Background process management
- Automatic restart on failure
### Interactive Features
**Tutorial (Super+T)**
- Step-by-step keyboard shortcut training
- Waits for correct input
- Progressive difficulty
**Demo Mode (Super+Shift+D)**
- Automated feature showcase
- Configurable timing
- Covers all major features
**Shortcuts Help (Super+S)**
- Quick reference overlay
- All keyboard bindings displayed
## Installation
### Dependencies
Required packages (via pkg-config):
- X11, Xext, Xinerama, Xrandr, Xft
Required libraries (via pkg-config):
- X11, Xext, Xinerama, Xrandr, Xft, Xi
- fontconfig
- libdbus-1
- libcurl
- libm
- libpng
- tesseract (optional, for OCR)
### Build Process
### Build
```bash
git clone https://github.com/your-repo/dwn.git
cd dwn
make deps # Auto-install dependencies (apt/dnf/pacman)
make # Build release version with -O2 optimization
make install # Install to /usr/local/bin
make # Build with -O2 optimization
make install # Install to /usr/local/bin (PREFIX configurable)
```
### Testing
```bash
make run # Launch in nested Xephyr window (safe testing)
make run # Launch in nested Xephyr window
```
### Build Targets
```bash
make # Release build with optimization
make debug # Debug build with -g -DDEBUG
make clean # Remove build artifacts
make format # Run clang-format
make check # Run cppcheck static analysis
```
| Target | Description |
|--------|-------------|
| `make` | Release build with optimization |
| `make debug` | Debug build with -g -DDEBUG |
| `make clean` | Remove build artifacts |
| `make format` | Run clang-format |
| `make check` | Run cppcheck static analysis |
## Configuration
Configuration file: `~/.config/dwn/config`
Configuration file: `~/.config/dwn/config` (INI format)
### General Settings
### General
```ini
[general]
@ -111,10 +251,10 @@ decorations = true
```ini
[appearance]
border_width = 0 # 0-50px (0 for seamless)
border_width = 0 # 0-50px
title_height = 28 # 0-100px
panel_height = 32 # 0-100px
gap = 0 # 0-100px (0 for seamless)
gap = 0 # 0-100px
font = fixed
```
@ -154,7 +294,7 @@ notification_bg = #111111
notification_fg = #00ffff
```
### AI Integration
### AI
```ini
[ai]
@ -169,7 +309,7 @@ export OPENROUTER_API_KEY=sk-or-v1-your-key
export EXA_API_KEY=your-exa-key
```
Get API keys:
API keys:
- OpenRouter: https://openrouter.ai/keys
- Exa: https://dashboard.exa.ai/api-keys
@ -178,11 +318,11 @@ Get API keys:
```ini
[autostart]
enabled = true
xdg_autostart = true # Scan /etc/xdg/autostart and ~/.config/autostart
xdg_autostart = true
path = ~/.config/dwn/autostart.d
```
### WebSocket API
### API
```ini
[api]
@ -190,18 +330,19 @@ enabled = true
port = 8777
```
### Demo Mode
### Demo
```ini
[demo]
step_delay = 4000 # 1000-30000ms between steps
ai_timeout = 15000 # 5000-60000ms for AI responses
window_timeout = 5000 # 1000-30000ms for window spawns
step_delay = 4000 # 1000-30000ms
ai_timeout = 15000 # 5000-60000ms
window_timeout = 5000 # 1000-30000ms
```
## Keyboard Shortcuts
### Application Launchers
| Shortcut | Action |
|----------|--------|
| `Ctrl+Alt+T` | Terminal |
@ -211,10 +352,11 @@ window_timeout = 5000 # 1000-30000ms for window spawns
| `Print` | Screenshot |
### Window Management
| Shortcut | Action |
|----------|--------|
| `Alt+F4` | Close window |
| `Alt+Tab` | Next window |
| `Alt+Tab` | Next window (MRU order) |
| `Alt+Shift+Tab` | Previous window |
| `Alt+F9` | Toggle minimize |
| `Alt+F10` | Toggle maximize |
@ -222,6 +364,7 @@ window_timeout = 5000 # 1000-30000ms for window spawns
| `Super+F9` | Toggle floating |
### Workspace Navigation
| Shortcut | Action |
|----------|--------|
| `F1` - `F9` | Switch to workspace 1-9 |
@ -230,85 +373,109 @@ window_timeout = 5000 # 1000-30000ms for window spawns
| `Ctrl+Alt+Left` | Previous workspace |
### Layout Control
| Shortcut | Action |
|----------|--------|
| `Super+Space` | Cycle layout (tiling/floating/monocle) |
| `Super+Space` | Cycle layout mode |
| `Super+H` | Shrink master area |
| `Super+L` | Expand master area |
| `Super+I` | Increase master count |
| `Super+D` | Decrease master count |
| `Super+D` | Decrease master count / Show desktop |
### Window Snapping
| Shortcut | Action |
|----------|--------|
| `Super+Left` | Snap left (50% or 100% width) |
| `Super+Right` | Snap right (50% or 100% width) |
| `Super+Up` | Snap top (50% or 100% height) |
| `Super+Down` | Snap bottom (50% or 100% height) |
Snapping is composable: Super+Left then Super+Up snaps to top-left quarter.
| `Super+Left` | Snap left (50% or 100%) |
| `Super+Right` | Snap right (50% or 100%) |
| `Super+Up` | Snap top (50% or 100%) |
| `Super+Down` | Snap bottom (50% or 100%) |
### AI Features
| Shortcut | Action |
|----------|--------|
| `Super+A` | Show AI context analysis |
| `Super+Shift+A` | AI command palette |
| `Super+A` | Context analysis |
| `Super+Shift+A` | Command palette |
| `Super+Shift+E` | Exa semantic search |
### News & Help
### News and Help
| Shortcut | Action |
|----------|--------|
| `Super+Return` | Open current news article |
| `Super+S` | Show shortcuts help |
| `Super+Return` | Open current article |
| `Super+S` | Shortcuts help |
| `Super+T` | Interactive tutorial |
| `Super+Shift+D` | Toggle demo mode |
| `Super+Backspace` | Quit DWN |
## WebSocket API
## Architecture
Connect to `ws://localhost:8777/ws` for programmatic control.
### Module Structure
### Commands
| Module | Responsibility |
|--------|----------------|
| main.c | X11 initialization, event loop, signal handling |
| client.c | Window management, focus, frame creation |
| workspace.c | Virtual desktop management, MRU stacks |
| layout.c | Tiling, floating, monocle algorithms |
| decorations.c | Title bars, borders, glow animations |
| panel.c | Top/bottom panel rendering, widgets |
| systray.c | XEmbed system tray, battery/volume widgets |
| notifications.c | D-Bus notification daemon |
| atoms.c | EWMH/ICCCM atom management |
| keys.c | Keyboard shortcut handling |
| config.c | INI configuration parser |
| ai.c | OpenRouter API, Exa search integration |
| news.c | News ticker with scrolling animation |
| screenshot.c | Screen capture API |
| ocr.c | Tesseract text extraction |
| autostart.c | XDG autostart implementation |
| services.c | Background service management |
| demo.c | Automated feature demonstration |
| api.c | WebSocket server |
| util.c | Logging, memory, string utilities, glow effects |
#### get_status
Returns window manager state.
```json
{"command": "get_status"}
```
### Design Patterns
#### get_workspaces
Returns array of workspace objects.
```json
{"command": "get_workspaces"}
```
**Encapsulation**
- Opaque pointer types hide internal structures
- Header exposes only public API
- Implementation details remain private
#### get_clients
Returns array of all managed windows.
```json
{"command": "get_clients"}
```
**Error Handling**
- Status code return values
- Output parameters for results
- Enum-based error codes
Response includes window ID, title, class, workspace, geometry, focus state.
**Resource Management**
- Goto cleanup pattern for multi-resource functions
- Every allocation has corresponding free
- XGrabServer/XUngrabServer for atomic X11 operations
#### switch_workspace
Change active workspace.
```json
{"command": "switch_workspace", "workspace": 2}
```
**Event Architecture**
- Select-based multiplexed I/O
- 60fps animation loop with 16ms timeout
- Async request queues for AI/screenshot/OCR
#### run_command
Execute shell command.
```json
{"command": "run_command", "exec": "firefox"}
```
**Naming Conventions**
- Module prefix for functions: `client_focus()`, `workspace_switch()`
- Snake_case for functions and variables
- CamelCase for types and structs
#### focus_client
Focus specific window by ID.
```json
{"command": "focus_client", "window": 12345678}
```
### Key Constants
### Python Example
| Constant | Value |
|----------|-------|
| MAX_CLIENTS | 256 |
| MAX_WORKSPACES | 9 |
| MAX_MONITORS | 8 |
| MAX_NOTIFICATIONS | 32 |
| MAX_KEYBINDINGS | 64 |
## WebSocket API Examples
### Python
```python
import websocket
@ -316,76 +483,50 @@ import json
ws = websocket.create_connection("ws://localhost:8777/ws")
ws.send(json.dumps({"command": "get_clients"}))
clients = json.loads(ws.recv())
ws.send(json.dumps({"command": "switch_workspace", "workspace": 3}))
ws.send(json.dumps({"command": "get_clients"}))
print(json.loads(ws.recv()))
ws.send(json.dumps({"command": "focus_client", "window": 12345678}))
ws.close()
```
## Module Architecture
### Response Format
### Core Modules (src/)
| Module | Responsibility |
|--------|----------------|
| main.c | X11 initialization, event loop, signal handling |
| client.c | Window management, focus, frame creation |
| workspace.c | Virtual desktop management |
| layout.c | Tiling, floating, monocle algorithms |
| decorations.c | Title bars and borders |
| panel.c | Top and bottom panel rendering |
| systray.c | XEmbed system tray, battery/volume/WiFi widgets |
| notifications.c | D-Bus notification daemon |
| atoms.c | EWMH/ICCCM atom management |
| keys.c | Keyboard shortcut handling |
| config.c | INI configuration parser |
| ai.c | OpenRouter API, Exa search integration |
| news.c | News ticker with scrolling animation |
| autostart.c | XDG autostart implementation |
| services.c | Background service management |
| demo.c | Automated feature demonstration |
| api.c | WebSocket server and JSON handler |
| util.c | Logging, memory, string utilities |
### Design Patterns
- **Encapsulation**: Opaque pointer types hide internal structures
- **Error Handling**: Status codes with output parameters
- **Resource Management**: Goto cleanup pattern for multi-resource functions
- **Modularity**: Single responsibility per source file
- **Naming**: Module prefix convention (e.g., `client_focus()`, `workspace_switch()`)
## Development
### Standards
- ANSI C (C89/C90) for maximum compatibility
- Snake_case for functions and variables
- CamelCase for types and structs
- No external dependencies beyond X11 and standard libraries
- Defensive programming: validate inputs, check malloc, null-terminate strings
### Code Style
```bash
make format # Apply clang-format
make check # Run cppcheck
```json
{
"status": "ok",
"data": {
"clients": [
{
"window": 12345678,
"title": "Firefox",
"class": "firefox",
"workspace": 1,
"x": 0, "y": 32,
"width": 960, "height": 540,
"focused": true
}
]
}
}
```
### Project Structure
## Project Structure
```
dwn/
├── src/ # Implementation
├── include/ # Headers
├── src/ # Implementation files
├── include/ # Header files
├── config/ # Configuration templates
├── scripts/ # Utilities and API examples
├── scripts/ # Utility scripts
├── examples/ # API usage examples
├── site/ # Documentation website
└── build/ # Build artifacts
```
## License
MIT License - see LICENSE file for details.
MIT License - see LICENSE file.

BIN
bin/dwn

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -223,11 +223,13 @@ typedef struct {
#define PHASE_OFFSET_CLOCK 2.4f
#define PHASE_OFFSET_STATS 3.2f
#define PHASE_OFFSET_TOP_MEM 4.0f
#define PHASE_OFFSET_TOP_CPU 4.4f
#define PHASE_OFFSET_DECORATIONS 4.8f
#define PHASE_OFFSET_SYSTRAY 5.6f
#define PHASE_OFFSET_NEWS 1.2f
#define PHASE_OFFSET_KEY_COUNTER 5.2f
#define PHASE_OFFSET_MOUSE_DIST 5.8f
#define PHASE_OFFSET_DISK_SPACE 6.2f
extern DWNState *dwn;

View File

@ -22,6 +22,7 @@ struct Panel {
int width, height;
bool visible;
Pixmap buffer;
bool dirty;
};
Panel *panel_create(PanelPosition position);
@ -31,6 +32,13 @@ void panels_cleanup(void);
void panel_render(Panel *panel);
void panel_render_all(void);
void panel_invalidate(void);
void panel_invalidate_taskbar(void);
void panel_invalidate_news(void);
void panel_set_news_region(int x, int width);
void panel_render_news_only(void);
bool panel_needs_render(void);
bool panel_news_needs_render(void);
void panel_render_workspaces(Panel *panel, int x, int *width);
void panel_render_taskbar(Panel *panel, int x, int *width);
void panel_render_clock(Panel *panel, int x, int *width);

View File

@ -1340,13 +1340,17 @@ unsigned long client_get_glow_text_color(Client *client)
return glow_color(client->text_glow.base_color, phase);
}
#define TEXT_GLOW_THROTTLE_MS 100
void client_update_animations(void)
{
if (dwn == NULL) {
return;
}
static long last_glow_update = 0;
long now = get_time_ms();
bool should_update_glow = (now - last_glow_update >= TEXT_GLOW_THROTTLE_MS);
bool needs_redraw = false;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
@ -1361,8 +1365,9 @@ void client_update_animations(void)
needs_redraw = true;
}
if (c->text_glow.active && c->workspace == (unsigned int)dwn->current_workspace) {
c->text_glow.phase += 0.005f * c->text_glow.speed;
if (should_update_glow && c->text_glow.active &&
c->workspace == (unsigned int)dwn->current_workspace) {
c->text_glow.phase += 0.05f * c->text_glow.speed;
if (c->text_glow.phase > 6.283f) {
c->text_glow.phase -= 6.283f;
}
@ -1370,7 +1375,11 @@ void client_update_animations(void)
}
}
if (should_update_glow) {
last_glow_update = now;
}
if (needs_redraw) {
panel_render_all();
panel_invalidate_taskbar();
}
}

View File

@ -19,6 +19,83 @@
#define RESIZE_EDGE 8
#define DEC_XFT_COLOR_CACHE_SIZE 8
typedef struct {
unsigned long pixel;
XftColor xft_color;
bool valid;
int lru_counter;
} DecCachedXftColor;
static DecCachedXftColor dec_xft_color_cache[DEC_XFT_COLOR_CACHE_SIZE];
static Visual *dec_cached_visual = NULL;
static int dec_xft_color_lru_counter = 0;
static XftDraw *dec_cached_xft_draw = NULL;
static Window dec_cached_window = None;
static XftDraw *dec_get_xft_draw(Window w)
{
if (dec_cached_window == w && dec_cached_xft_draw != NULL) {
return dec_cached_xft_draw;
}
if (dec_cached_xft_draw != NULL) {
XftDrawDestroy(dec_cached_xft_draw);
}
if (dec_cached_visual == NULL) {
dec_cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
dec_cached_xft_draw = XftDrawCreate(dwn->display, w, dec_cached_visual, dwn->colormap);
dec_cached_window = w;
return dec_cached_xft_draw;
}
static XftColor *dec_get_cached_xft_color(unsigned long color)
{
if (dec_cached_visual == NULL) {
dec_cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
for (int i = 0; i < DEC_XFT_COLOR_CACHE_SIZE; i++) {
if (dec_xft_color_cache[i].valid && dec_xft_color_cache[i].pixel == color) {
dec_xft_color_cache[i].lru_counter = ++dec_xft_color_lru_counter;
return &dec_xft_color_cache[i].xft_color;
}
}
int oldest_idx = 0;
int oldest_lru = dec_xft_color_cache[0].lru_counter;
for (int i = 1; i < DEC_XFT_COLOR_CACHE_SIZE; i++) {
if (!dec_xft_color_cache[i].valid) {
oldest_idx = i;
break;
}
if (dec_xft_color_cache[i].lru_counter < oldest_lru) {
oldest_lru = dec_xft_color_cache[i].lru_counter;
oldest_idx = i;
}
}
if (dec_xft_color_cache[oldest_idx].valid) {
XftColorFree(dwn->display, dec_cached_visual, dwn->colormap,
&dec_xft_color_cache[oldest_idx].xft_color);
}
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, dec_cached_visual, dwn->colormap,
&render_color, &dec_xft_color_cache[oldest_idx].xft_color);
dec_xft_color_cache[oldest_idx].pixel = color;
dec_xft_color_cache[oldest_idx].valid = true;
dec_xft_color_cache[oldest_idx].lru_counter = ++dec_xft_color_lru_counter;
return &dec_xft_color_cache[oldest_idx].xft_color;
}
void decorations_init(void)
{
@ -117,27 +194,12 @@ void decorations_render_title_bar(Client *client, bool focused)
strncat(display_title, "...", sizeof(display_title) - strlen(display_title) - 1);
}
XftDraw *xft_draw = XftDrawCreate(dpy, client->frame,
DefaultVisual(dpy, dwn->screen),
dwn->colormap);
XftDraw *xft_draw = dec_get_xft_draw(client->frame);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((fg_color >> 16) & 0xFF) * 257;
render_color.green = ((fg_color >> 8) & 0xFF) * 257;
render_color.blue = (fg_color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dpy, DefaultVisual(dpy, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, title_font,
XftColor *xft_color = dec_get_cached_xft_color(fg_color);
XftDrawStringUtf8(xft_draw, xft_color, title_font,
text_x, text_y,
(const FcChar8 *)display_title, strlen(display_title));
XftColorFree(dpy, DefaultVisual(dpy, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
}
}
}
@ -334,26 +396,11 @@ void decorations_draw_text(Window window, GC gc, int x, int y,
}
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, window,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
XftDraw *xft_draw = dec_get_xft_draw(window);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
XftColor *xft_color = dec_get_cached_xft_color(color);
XftDrawStringUtf8(xft_draw, xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, strlen(text));
XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}

View File

@ -164,10 +164,11 @@ static void init_xinput2(void)
XISetMask(mask, XI_RawKeyPress);
XISetMask(mask, XI_RawKeyRelease);
XISetMask(mask, XI_RawMotion);
XISelectEvents(dwn->display, dwn->root, &evmask, 1);
LOG_INFO("XInput2 initialized for raw key event monitoring");
LOG_INFO("XInput2 initialized for raw key and mouse event monitoring");
}
static void handle_xi2_event(XGenericEventCookie *cookie)
@ -198,6 +199,20 @@ static void handle_xi2_event(XGenericEventCookie *cookie)
dwn->key_delete_flicker_time = get_time_ms();
}
}
} else if (cookie->evtype == XI_RawMotion) {
XIRawEvent *raw = (XIRawEvent *)cookie->data;
if (raw->valuators.mask_len > 0) {
double dx = 0.0, dy = 0.0;
double *values = raw->raw_values;
int idx = 0;
if (XIMaskIsSet(raw->valuators.mask, 0)) {
dx = values[idx++];
}
if (XIMaskIsSet(raw->valuators.mask, 1)) {
dy = values[idx];
}
dwn->mouse_distance_pixels += sqrt(dx * dx + dy * dy);
}
}
XFreeEventData(dwn->display, cookie);
@ -393,7 +408,7 @@ static void handle_property_notify(XPropertyEvent *ev)
if (ev->atom == icccm.WM_NAME || ev->atom == ewmh.NET_WM_NAME) {
client_update_title(c);
panel_render_all();
panel_invalidate_taskbar();
}
}
@ -1024,21 +1039,6 @@ void dwn_run(void)
client_update_animations();
Window root_ret, child_ret;
int root_x, root_y, win_x, win_y;
unsigned int mask_ret;
if (XQueryPointer(dwn->display, dwn->root, &root_ret, &child_ret,
&root_x, &root_y, &win_x, &win_y, &mask_ret)) {
if (dwn->mouse_tracking_initialized) {
int dx = root_x - dwn->last_mouse_x;
int dy = root_y - dwn->last_mouse_y;
dwn->mouse_distance_pixels += sqrt((double)(dx * dx + dy * dy));
}
dwn->last_mouse_x = root_x;
dwn->last_mouse_y = root_y;
dwn->mouse_tracking_initialized = true;
}
notifications_update();
if (dwn->pending_focus_client != NULL) {
@ -1053,12 +1053,23 @@ void dwn_run(void)
}
}
if (now - last_news_update >= 16) {
bool news_rendered = false;
if (now - last_news_update >= 50) {
news_update();
panel_render_all();
if (panel_news_needs_render()) {
panel_render_news_only();
news_rendered = true;
}
last_news_update = now;
}
if (panel_needs_render()) {
panel_render_all();
} else if (news_rendered) {
XFlush(dwn->display);
}
if (now - last_clock_update >= 1000) {
panel_update_clock();
panel_update_system_stats();
@ -1075,7 +1086,7 @@ void dwn_run(void)
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 16000;
tv.tv_usec = 50000;
int max_fd = x11_fd;
if (dbus_fd > max_fd) max_fd = dbus_fd;

View File

@ -261,6 +261,86 @@ static int parse_news_json(const char *json_str)
static int news_find_article_at_x(int click_x);
static XftDraw *news_xft_draw = NULL;
static Drawable news_xft_drawable = None;
static Visual *news_cached_visual = NULL;
#define NEWS_XFT_COLOR_CACHE_SIZE 8
typedef struct {
unsigned long pixel;
XftColor xft_color;
bool valid;
int lru_counter;
} NewsCachedColor;
static NewsCachedColor news_color_cache[NEWS_XFT_COLOR_CACHE_SIZE];
static int news_color_lru_counter = 0;
static int cached_separator_width = -1;
static XftDraw *news_get_xft_draw(Drawable d)
{
if (news_xft_drawable == d && news_xft_draw != NULL) {
return news_xft_draw;
}
if (news_xft_draw != NULL) {
XftDrawDestroy(news_xft_draw);
}
if (news_cached_visual == NULL) {
news_cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
news_xft_draw = XftDrawCreate(dwn->display, d, news_cached_visual, dwn->colormap);
news_xft_drawable = d;
return news_xft_draw;
}
static XftColor *news_get_cached_color(unsigned long color)
{
if (news_cached_visual == NULL) {
news_cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
for (int i = 0; i < NEWS_XFT_COLOR_CACHE_SIZE; i++) {
if (news_color_cache[i].valid && news_color_cache[i].pixel == color) {
news_color_cache[i].lru_counter = ++news_color_lru_counter;
return &news_color_cache[i].xft_color;
}
}
int oldest_idx = 0;
int oldest_lru = news_color_cache[0].lru_counter;
for (int i = 1; i < NEWS_XFT_COLOR_CACHE_SIZE; i++) {
if (!news_color_cache[i].valid) {
oldest_idx = i;
break;
}
if (news_color_cache[i].lru_counter < oldest_lru) {
oldest_lru = news_color_cache[i].lru_counter;
oldest_idx = i;
}
}
if (news_color_cache[oldest_idx].valid) {
XftColorFree(dwn->display, news_cached_visual, dwn->colormap,
&news_color_cache[oldest_idx].xft_color);
}
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, news_cached_visual, dwn->colormap,
&render_color, &news_color_cache[oldest_idx].xft_color);
news_color_cache[oldest_idx].pixel = color;
news_color_cache[oldest_idx].valid = true;
news_color_cache[oldest_idx].lru_counter = ++news_color_lru_counter;
return &news_color_cache[oldest_idx].xft_color;
}
static int news_text_width(const char *text)
{
if (dwn == NULL || text == NULL) return 0;
@ -285,9 +365,7 @@ static void news_draw_text_clipped(Drawable d, int x, int y, const char *text,
if (dwn == NULL || dwn->display == NULL || text == NULL) return;
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
XftDraw *xft_draw = news_get_xft_draw(d);
if (xft_draw != NULL) {
if (clip != NULL) {
Region region = XCreateRegion();
@ -296,25 +374,13 @@ static void news_draw_text_clipped(Drawable d, int x, int y, const char *text,
XDestroyRegion(region);
}
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
XftColor *xft_color = news_get_cached_color(color);
XftDrawStringUtf8(xft_draw, xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, strlen(text));
XftColorFree(dwn->display,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
if (clip != NULL) {
XftDrawSetClip(xft_draw, None);
}
return;
}
}
@ -349,7 +415,10 @@ static void news_recalc_widths(void)
return;
}
int separator_width = news_text_width(NEWS_SEPARATOR);
if (cached_separator_width < 0) {
cached_separator_width = news_text_width(NEWS_SEPARATOR);
}
int separator_width = cached_separator_width;
news_state.total_width = 0;
for (int i = 0; i < news_state.article_count; i++) {
@ -479,16 +548,22 @@ void news_update(void)
pthread_mutex_lock(&news_mutex);
}
bool scrolled = false;
if (!news_state.interactive_mode && news_state.article_count > 0) {
if (news_state.last_scroll_update > 0) {
long delta_ms = now - news_state.last_scroll_update;
double scroll_amount = (SCROLL_SPEED_PPS * delta_ms) / 1000.0;
news_state.scroll_offset += scroll_amount;
scrolled = true;
}
news_state.last_scroll_update = now;
}
pthread_mutex_unlock(&news_mutex);
if (scrolled) {
panel_invalidate_news();
}
}
void news_next_article(void)
@ -554,7 +629,10 @@ static int news_find_article_at_x(int click_x)
return -1;
}
int separator_width = news_text_width(NEWS_SEPARATOR);
if (cached_separator_width < 0) {
cached_separator_width = news_text_width(NEWS_SEPARATOR);
}
int separator_width = cached_separator_width;
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
int rel_x = click_x - news_state.render_x;
@ -618,7 +696,10 @@ void news_render(Panel *panel, int x, int max_width, int *used_width)
news_state.render_x = x;
news_state.render_width = max_width;
int separator_width = news_text_width(NEWS_SEPARATOR);
if (cached_separator_width < 0) {
cached_separator_width = news_text_width(NEWS_SEPARATOR);
}
int separator_width = cached_separator_width;
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
XRectangle clip_rect = { x, 0, max_width, panel->height };

View File

@ -21,19 +21,60 @@
#include <stdlib.h>
#include <dirent.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/statvfs.h>
#include <X11/Xft/Xft.h>
#define PANEL_PADDING 12
#define WIDGET_SPACING 16
#define WORKSPACE_WIDTH 36
#define TASKBAR_ITEM_WIDTH 180
#define TOP_PROCESS_NAME_MAX_LEN 12
#define CLOCK_FORMAT "%H:%M:%S"
#define DATE_FORMAT "%Y-%m-%d"
#define XFT_COLOR_CACHE_SIZE 16
#define PROCESS_SCAN_INTERVAL_MS 30000
#define PROC_CPU_HISTORY_SIZE 1024
#define DISK_UPDATE_INTERVAL_MS 60000
static char clock_buffer[32] = "";
static char date_buffer[32] = "";
static double disk_free_gb = 0.0;
static double disk_total_gb = 0.0;
static long disk_last_update = 0;
static bool panels_dirty = true;
static int news_region_x = 0;
static int news_region_width = 0;
static bool news_region_dirty = false;
typedef struct {
unsigned long pixel;
XftColor xft_color;
bool valid;
int lru_counter;
} CachedXftColor;
static CachedXftColor xft_color_cache[XFT_COLOR_CACHE_SIZE];
static Visual *cached_visual = NULL;
static int xft_color_lru_counter = 0;
static XftDraw *cached_xft_draw = NULL;
static Drawable cached_drawable = None;
static long last_memory_scan = 0;
typedef struct {
int pid;
unsigned long long prev_utime;
unsigned long long prev_stime;
unsigned long long prev_total_cpu;
} ProcCpuState;
typedef struct {
int cpu_percent;
int mem_percent;
@ -44,18 +85,32 @@ typedef struct {
float load_15min;
unsigned long long prev_idle;
unsigned long long prev_total;
char top_mem_process[64];
int top_mem_percent;
char top_mem_processes[3][64];
int top_mem_percents[3];
int top_mem_count;
char top_cpu_processes[3][64];
int top_cpu_percents[3];
int top_cpu_count;
int process_display_index;
int process_category;
long process_last_cycle;
} SystemStats;
static ProcCpuState proc_cpu_history[PROC_CPU_HISTORY_SIZE];
static int proc_cpu_history_count = 0;
static unsigned long long prev_total_cpu_time = 0;
static SystemStats sys_stats = {0};
static void panel_render_system_stats(Panel *panel, int x, int *width);
static int panel_calculate_stats_width(void);
static void panel_render_top_mem_process(Panel *panel, int x, int *width);
static void panel_render_top_process(Panel *panel, int x, int *width);
static int panel_calculate_top_process_width(void);
static void panel_render_key_counter(Panel *panel, int x, int *width);
static void panel_render_mouse_distance(Panel *panel, int x, int *width);
static void update_top_memory_process(void);
static void panel_render_disk_space(Panel *panel, int x, int *width);
static void update_disk_space(void);
static void update_top_processes(void);
static int panel_text_width(const char *text, int len)
@ -76,32 +131,79 @@ static int panel_text_width(const char *text, int len)
return 0;
}
static XftDraw *panel_get_xft_draw(Drawable d)
{
if (cached_drawable == d && cached_xft_draw != NULL) {
return cached_xft_draw;
}
if (cached_xft_draw != NULL) {
XftDrawDestroy(cached_xft_draw);
}
if (cached_visual == NULL) {
cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
cached_xft_draw = XftDrawCreate(dwn->display, d, cached_visual, dwn->colormap);
cached_drawable = d;
return cached_xft_draw;
}
static XftColor *panel_get_cached_xft_color(unsigned long color)
{
if (cached_visual == NULL) {
cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
for (int i = 0; i < XFT_COLOR_CACHE_SIZE; i++) {
if (xft_color_cache[i].valid && xft_color_cache[i].pixel == color) {
xft_color_cache[i].lru_counter = ++xft_color_lru_counter;
return &xft_color_cache[i].xft_color;
}
}
int oldest_idx = 0;
int oldest_lru = xft_color_cache[0].lru_counter;
for (int i = 1; i < XFT_COLOR_CACHE_SIZE; i++) {
if (!xft_color_cache[i].valid) {
oldest_idx = i;
break;
}
if (xft_color_cache[i].lru_counter < oldest_lru) {
oldest_lru = xft_color_cache[i].lru_counter;
oldest_idx = i;
}
}
if (xft_color_cache[oldest_idx].valid) {
XftColorFree(dwn->display, cached_visual, dwn->colormap,
&xft_color_cache[oldest_idx].xft_color);
}
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, cached_visual, dwn->colormap,
&render_color, &xft_color_cache[oldest_idx].xft_color);
xft_color_cache[oldest_idx].pixel = color;
xft_color_cache[oldest_idx].valid = true;
xft_color_cache[oldest_idx].lru_counter = ++xft_color_lru_counter;
return &xft_color_cache[oldest_idx].xft_color;
}
static void panel_draw_text_with_font(Drawable d, int x, int y, const char *text,
int len, unsigned long color, XftFont *font)
{
if (text == NULL || dwn == NULL || dwn->display == NULL) return;
if (font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
XftDraw *xft_draw = panel_get_xft_draw(d);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, font,
XftColor *xft_color = panel_get_cached_xft_color(color);
XftDrawStringUtf8(xft_draw, xft_color, font,
x, y, (const FcChar8 *)text, len);
XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}
@ -317,12 +419,13 @@ void panel_render(Panel *panel)
int mouse_dist_x = key_counter_x + key_counter_width + WIDGET_SPACING;
panel_render_mouse_distance(panel, mouse_dist_x, &mouse_dist_width);
int top_mem_width = 0;
int top_mem_x = mouse_dist_x + mouse_dist_width + WIDGET_SPACING;
panel_render_top_mem_process(panel, top_mem_x, &top_mem_width);
int disk_space_width = 0;
int disk_space_x = mouse_dist_x + mouse_dist_width + WIDGET_SPACING;
panel_render_disk_space(panel, disk_space_x, &disk_space_width);
int clock_width = panel_text_width(clock_buffer, strlen(clock_buffer));
int stats_width = panel_calculate_stats_width();
int top_proc_width = panel_calculate_top_process_width();
int clock_x = panel->width - clock_width - PANEL_PADDING;
panel_render_clock(panel, clock_x, &width);
@ -330,9 +433,14 @@ void panel_render(Panel *panel)
int stats_x = clock_x - stats_width - WIDGET_SPACING;
panel_render_system_stats(panel, stats_x, &width);
int news_start = top_mem_x + top_mem_width + WIDGET_SPACING;
int news_max_width = stats_x - news_start - WIDGET_SPACING;
int top_proc_x = stats_x - top_proc_width - WIDGET_SPACING;
int actual_top_proc_width = 0;
panel_render_top_process(panel, top_proc_x, &actual_top_proc_width);
int news_start = disk_space_x + disk_space_width + WIDGET_SPACING;
int news_max_width = top_proc_x - news_start - WIDGET_SPACING;
if (news_max_width > 100) {
panel_set_news_region(news_start, news_max_width);
int news_width = 0;
news_render(panel, news_start, news_max_width, &news_width);
}
@ -340,18 +448,20 @@ void panel_render(Panel *panel)
XCopyArea(dpy, panel->buffer, panel->window, dwn->gc,
0, 0, panel->width, panel->height, 0, 0);
XFlush(dpy);
}
void panel_render_all(void)
{
if (dwn->top_panel != NULL) {
panel_render(dwn->top_panel);
dwn->top_panel->dirty = false;
}
if (dwn->bottom_panel != NULL) {
panel_render(dwn->bottom_panel);
dwn->bottom_panel->dirty = false;
}
panels_dirty = false;
XFlush(dwn->display);
}
void panel_render_workspaces(Panel *panel, int x, int *width)
@ -456,9 +566,9 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
int max_text_width = item_width - 8;
bool truncated = false;
while (panel_text_width(title, strlen(title)) > max_text_width && strlen(title) > 3) {
size_t len = strlen(title);
size_t cut = len - 1;
size_t title_len = strlen(title);
while (panel_text_width(title, (int)title_len) > max_text_width && title_len > 3) {
size_t cut = title_len - 1;
while (cut > 0 && (title[cut] & 0xC0) == 0x80) {
cut--;
}
@ -470,10 +580,12 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
cut = sizeof(title) - 4;
}
title[cut] = '\0';
title_len = cut;
truncated = true;
}
if (truncated) {
strncat(title, "...", sizeof(title) - strlen(title) - 1);
strncat(title, "...", sizeof(title) - title_len - 1);
title_len += 3;
}
unsigned long fg;
@ -805,22 +917,100 @@ void panel_update_system_stats(void)
fclose(fp);
}
update_top_memory_process();
long now = get_time_ms();
if (now - last_memory_scan >= PROCESS_SCAN_INTERVAL_MS) {
update_top_processes();
last_memory_scan = now;
}
static int prev_cpu = -1;
static int prev_mem = -1;
bool stats_changed = (prev_cpu != sys_stats.cpu_percent) ||
(prev_mem != sys_stats.mem_percent);
if (stats_changed) {
prev_cpu = sys_stats.cpu_percent;
prev_mem = sys_stats.mem_percent;
panel_invalidate();
}
}
static void update_top_memory_process(void)
static ProcCpuState *find_cpu_history(int pid)
{
for (int i = 0; i < proc_cpu_history_count; i++) {
if (proc_cpu_history[i].pid == pid) {
return &proc_cpu_history[i];
}
}
return NULL;
}
static ProcCpuState *add_cpu_history(int pid)
{
if (proc_cpu_history_count < PROC_CPU_HISTORY_SIZE) {
ProcCpuState *state = &proc_cpu_history[proc_cpu_history_count++];
state->pid = pid;
state->prev_utime = 0;
state->prev_stime = 0;
state->prev_total_cpu = 0;
return state;
}
char path[32];
for (int i = 0; i < PROC_CPU_HISTORY_SIZE; i++) {
snprintf(path, sizeof(path), "/proc/%d", proc_cpu_history[i].pid);
if (access(path, F_OK) != 0) {
proc_cpu_history[i].pid = pid;
proc_cpu_history[i].prev_utime = 0;
proc_cpu_history[i].prev_stime = 0;
proc_cpu_history[i].prev_total_cpu = 0;
return &proc_cpu_history[i];
}
}
proc_cpu_history[0].pid = pid;
proc_cpu_history[0].prev_utime = 0;
proc_cpu_history[0].prev_stime = 0;
proc_cpu_history[0].prev_total_cpu = 0;
return &proc_cpu_history[0];
}
static unsigned long long read_total_cpu_time(void)
{
FILE *fp = fopen("/proc/stat", "r");
if (fp == NULL) return 0;
char line[256];
unsigned long long total = 0;
if (fgets(line, sizeof(line), fp) != NULL) {
unsigned long long user, nice, system, idle, iowait, irq, softirq, steal;
if (sscanf(line, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
&user, &nice, &system, &idle, &iowait, &irq, &softirq, &steal) >= 4) {
total = user + nice + system + idle + iowait + irq + softirq + steal;
}
}
fclose(fp);
return total;
}
static void update_top_processes(void)
{
DIR *proc_dir = opendir("/proc");
if (proc_dir == NULL) {
sys_stats.top_mem_process[0] = '\0';
sys_stats.top_mem_percent = 0;
sys_stats.top_mem_count = 0;
sys_stats.top_cpu_count = 0;
return;
}
unsigned long max_rss = 0;
char max_name[64] = "";
unsigned long long current_total_cpu = read_total_cpu_time();
unsigned long long cpu_delta = current_total_cpu - prev_total_cpu_time;
unsigned long top_rss[3] = {0, 0, 0};
char top_mem_names[3][64] = {"", "", ""};
unsigned long mem_total_kb = (unsigned long)sys_stats.mem_total_mb * 1024;
int top_cpu_pcts[3] = {0, 0, 0};
char top_cpu_names[3][64] = {"", "", ""};
struct dirent *entry;
while ((entry = readdir(proc_dir)) != NULL) {
bool is_pid = true;
@ -834,8 +1024,10 @@ static void update_top_memory_process(void)
}
if (!is_pid || name_len > 20) continue;
int pid = atoi(entry->d_name);
char status_path[64];
snprintf(status_path, sizeof(status_path), "/proc/%.20s/status", entry->d_name);
snprintf(status_path, sizeof(status_path), "/proc/%d/status", pid);
FILE *fp = fopen(status_path, "r");
if (fp == NULL) continue;
@ -858,19 +1050,91 @@ static void update_top_memory_process(void)
}
fclose(fp);
if (rss_kb > max_rss && name[0] != '\0') {
max_rss = rss_kb;
snprintf(max_name, sizeof(max_name), "%s", name);
if (rss_kb > 0 && name[0] != '\0') {
for (int i = 0; i < 3; i++) {
if (rss_kb > top_rss[i]) {
for (int j = 2; j > i; j--) {
top_rss[j] = top_rss[j - 1];
memcpy(top_mem_names[j], top_mem_names[j - 1], 64);
}
top_rss[i] = rss_kb;
memcpy(top_mem_names[i], name, 64);
break;
}
}
}
char stat_path[64];
snprintf(stat_path, sizeof(stat_path), "/proc/%d/stat", pid);
fp = fopen(stat_path, "r");
if (fp == NULL) continue;
unsigned long long utime = 0, stime = 0;
char stat_line[512];
if (fgets(stat_line, sizeof(stat_line), fp) != NULL) {
char *close_paren = strrchr(stat_line, ')');
if (close_paren != NULL) {
if (sscanf(close_paren + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %llu %llu",
&utime, &stime) == 2) {
ProcCpuState *hist = find_cpu_history(pid);
if (hist == NULL) {
hist = add_cpu_history(pid);
}
if (hist->prev_total_cpu > 0 && cpu_delta > 0) {
unsigned long long proc_delta = (utime + stime) - (hist->prev_utime + hist->prev_stime);
int num_cpus = (int)sysconf(_SC_NPROCESSORS_ONLN);
if (num_cpus < 1) num_cpus = 1;
int cpu_pct = (int)((proc_delta * 100 * (unsigned long long)num_cpus) / cpu_delta);
if (cpu_pct > 100) cpu_pct = 100;
for (int i = 0; i < 3; i++) {
if (cpu_pct > top_cpu_pcts[i] || top_cpu_names[i][0] == '\0') {
if (cpu_pct > top_cpu_pcts[i]) {
for (int j = 2; j > i; j--) {
top_cpu_pcts[j] = top_cpu_pcts[j - 1];
memcpy(top_cpu_names[j], top_cpu_names[j - 1], 64);
}
}
top_cpu_pcts[i] = cpu_pct;
memcpy(top_cpu_names[i], name, 64);
break;
}
}
}
hist->prev_utime = utime;
hist->prev_stime = stime;
hist->prev_total_cpu = current_total_cpu;
}
}
}
fclose(fp);
}
closedir(proc_dir);
snprintf(sys_stats.top_mem_process, sizeof(sys_stats.top_mem_process), "%s", max_name);
prev_total_cpu_time = current_total_cpu;
if (mem_total_kb > 0) {
sys_stats.top_mem_percent = (int)((max_rss * 100) / mem_total_kb);
} else {
sys_stats.top_mem_percent = 0;
sys_stats.top_mem_count = 0;
for (int i = 0; i < 3; i++) {
if (top_mem_names[i][0] != '\0') {
memcpy(sys_stats.top_mem_processes[i], top_mem_names[i], 64);
if (mem_total_kb > 0) {
sys_stats.top_mem_percents[i] = (int)((top_rss[i] * 100) / mem_total_kb);
} else {
sys_stats.top_mem_percents[i] = 0;
}
sys_stats.top_mem_count++;
}
}
sys_stats.top_cpu_count = 0;
for (int i = 0; i < 3; i++) {
if (top_cpu_names[i][0] != '\0') {
memcpy(sys_stats.top_cpu_processes[i], top_cpu_names[i], 64);
sys_stats.top_cpu_percents[i] = top_cpu_pcts[i];
sys_stats.top_cpu_count++;
}
}
}
@ -1100,7 +1364,23 @@ static void panel_render_mouse_distance(Panel *panel, int x, int *width)
*width = panel_text_width(buf, len);
}
static void panel_render_top_mem_process(Panel *panel, int x, int *width)
static void update_disk_space(void)
{
long now = get_time_ms();
if (now - disk_last_update < DISK_UPDATE_INTERVAL_MS && disk_last_update > 0) {
return;
}
struct statvfs stat;
if (statvfs("/", &stat) == 0) {
unsigned long long block_size = stat.f_frsize;
disk_free_gb = (double)(stat.f_bavail * block_size) / (1024.0 * 1024.0 * 1024.0);
disk_total_gb = (double)(stat.f_blocks * block_size) / (1024.0 * 1024.0 * 1024.0);
}
disk_last_update = now;
}
static void panel_render_disk_space(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
*width = 0;
@ -1110,30 +1390,247 @@ static void panel_render_top_mem_process(Panel *panel, int x, int *width)
*width = 0;
return;
}
if (sys_stats.top_mem_process[0] == '\0') {
*width = 0;
return;
}
update_disk_space();
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
char buf[32];
int len;
if (disk_total_gb >= 1000.0) {
len = snprintf(buf, sizeof(buf), "Disk:%.1fT/%.1fT",
disk_free_gb / 1024.0, disk_total_gb / 1024.0);
} else {
len = snprintf(buf, sizeof(buf), "Disk:%.0fG/%.0fG",
disk_free_gb, disk_total_gb);
}
int used_percent = 0;
if (disk_total_gb > 0.0) {
used_percent = (int)(((disk_total_gb - disk_free_gb) / disk_total_gb) * 100.0);
}
unsigned long color = colors->panel_fg;
if (sys_stats.top_mem_percent >= 50) {
if (used_percent >= 90) {
color = colors->workspace_urgent;
} else if (sys_stats.top_mem_percent >= 25) {
} else if (used_percent >= 75) {
color = 0xFFA500;
}
color = ambient_glow_bg(color, dwn->ambient_phase + PHASE_OFFSET_TOP_MEM);
char buf[128];
int len = snprintf(buf, sizeof(buf), "%s %d%%",
sys_stats.top_mem_process, sys_stats.top_mem_percent);
panel_draw_text(panel->buffer, x, text_y, buf, len, color);
unsigned long fg = ambient_glow_bg(color, dwn->ambient_phase + PHASE_OFFSET_DISK_SPACE);
panel_draw_text(panel->buffer, x, text_y, buf, len, fg);
*width = panel_text_width(buf, len);
}
static int panel_calculate_top_process_width(void)
{
if (dwn->xft_font == NULL && dwn->font == NULL) return 0;
int mem_count = sys_stats.top_mem_count;
int cpu_count = sys_stats.top_cpu_count;
if (mem_count == 0 && cpu_count == 0) return 0;
int idx = sys_stats.process_display_index;
char buf[128];
char name_buf[TOP_PROCESS_NAME_MAX_LEN + 1];
const char *proc_name;
size_t name_len;
int len;
int total = 0;
if (mem_count > 0) {
int mem_idx = (idx >= mem_count) ? 0 : idx;
proc_name = sys_stats.top_mem_processes[mem_idx];
name_len = strlen(proc_name);
if (name_len > TOP_PROCESS_NAME_MAX_LEN) {
strncpy(name_buf, proc_name, TOP_PROCESS_NAME_MAX_LEN - 2);
name_buf[TOP_PROCESS_NAME_MAX_LEN - 2] = '\0';
strncat(name_buf, "..", 3);
proc_name = name_buf;
}
len = snprintf(buf, sizeof(buf), "M:%s %d%%", proc_name, sys_stats.top_mem_percents[mem_idx]);
total += panel_text_width(buf, len);
}
if (cpu_count > 0) {
if (total > 0) total += WIDGET_SPACING;
int cpu_idx = (idx >= cpu_count) ? 0 : idx;
proc_name = sys_stats.top_cpu_processes[cpu_idx];
name_len = strlen(proc_name);
if (name_len > TOP_PROCESS_NAME_MAX_LEN) {
strncpy(name_buf, proc_name, TOP_PROCESS_NAME_MAX_LEN - 2);
name_buf[TOP_PROCESS_NAME_MAX_LEN - 2] = '\0';
strncat(name_buf, "..", 3);
proc_name = name_buf;
}
len = snprintf(buf, sizeof(buf), "C:%s %d%%", proc_name, sys_stats.top_cpu_percents[cpu_idx]);
total += panel_text_width(buf, len);
}
return total;
}
static void panel_render_top_process(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
*width = 0;
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
*width = 0;
return;
}
int mem_count = sys_stats.top_mem_count;
int cpu_count = sys_stats.top_cpu_count;
if (mem_count == 0 && cpu_count == 0) {
*width = 0;
return;
}
long now = get_time_ms();
if (now - sys_stats.process_last_cycle >= 3000) {
sys_stats.process_display_index++;
if (sys_stats.process_display_index >= 3) {
sys_stats.process_display_index = 0;
}
sys_stats.process_last_cycle = now;
}
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
int idx = sys_stats.process_display_index;
char buf[128];
char name_buf[TOP_PROCESS_NAME_MAX_LEN + 1];
int len;
unsigned long color;
const char *proc_name;
size_t name_len;
int text_width;
int total_width = panel_calculate_top_process_width();
int right_edge = x + total_width;
int current_right = right_edge;
if (cpu_count > 0) {
int cpu_idx = (idx >= cpu_count) ? 0 : idx;
int percent = sys_stats.top_cpu_percents[cpu_idx];
color = colors->panel_fg;
if (percent >= 50) {
color = 0x4169E1;
} else if (percent >= 25) {
color = 0x00CED1;
}
color = ambient_glow_bg(color, dwn->ambient_phase + PHASE_OFFSET_TOP_CPU);
proc_name = sys_stats.top_cpu_processes[cpu_idx];
name_len = strlen(proc_name);
if (name_len > TOP_PROCESS_NAME_MAX_LEN) {
strncpy(name_buf, proc_name, TOP_PROCESS_NAME_MAX_LEN - 2);
name_buf[TOP_PROCESS_NAME_MAX_LEN - 2] = '\0';
strncat(name_buf, "..", 3);
proc_name = name_buf;
}
len = snprintf(buf, sizeof(buf), "C:%s %d%%", proc_name, percent);
text_width = panel_text_width(buf, len);
int text_x = current_right - text_width;
panel_draw_text(panel->buffer, text_x, text_y, buf, len, color);
current_right = text_x - WIDGET_SPACING;
}
if (mem_count > 0) {
int mem_idx = (idx >= mem_count) ? 0 : idx;
int percent = sys_stats.top_mem_percents[mem_idx];
color = colors->panel_fg;
if (percent >= 50) {
color = colors->workspace_urgent;
} else if (percent >= 25) {
color = 0xFFA500;
}
color = ambient_glow_bg(color, dwn->ambient_phase + PHASE_OFFSET_TOP_MEM);
proc_name = sys_stats.top_mem_processes[mem_idx];
name_len = strlen(proc_name);
if (name_len > TOP_PROCESS_NAME_MAX_LEN) {
strncpy(name_buf, proc_name, TOP_PROCESS_NAME_MAX_LEN - 2);
name_buf[TOP_PROCESS_NAME_MAX_LEN - 2] = '\0';
strncat(name_buf, "..", 3);
proc_name = name_buf;
}
len = snprintf(buf, sizeof(buf), "M:%s %d%%", proc_name, percent);
text_width = panel_text_width(buf, len);
int text_x = current_right - text_width;
panel_draw_text(panel->buffer, text_x, text_y, buf, len, color);
}
*width = total_width;
}
void panel_invalidate(void)
{
panels_dirty = true;
if (dwn->top_panel) dwn->top_panel->dirty = true;
if (dwn->bottom_panel) dwn->bottom_panel->dirty = true;
}
void panel_invalidate_taskbar(void)
{
panels_dirty = true;
if (dwn->top_panel) dwn->top_panel->dirty = true;
}
void panel_invalidate_news(void)
{
news_region_dirty = true;
}
void panel_set_news_region(int x, int width)
{
news_region_x = x;
news_region_width = width;
}
void panel_render_news_only(void)
{
if (dwn->bottom_panel == NULL || !news_region_dirty) {
return;
}
if (news_region_width <= 0) {
news_region_dirty = false;
return;
}
Panel *panel = dwn->bottom_panel;
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
unsigned long bg = ambient_glow_bg(colors->panel_bg,
dwn->ambient_phase + PHASE_OFFSET_PANEL_BG);
XSetForeground(dpy, dwn->gc, bg);
XFillRectangle(dpy, panel->buffer, dwn->gc,
news_region_x, 0, (unsigned int)news_region_width, (unsigned int)panel->height);
int width;
news_render(panel, news_region_x, news_region_width, &width);
XCopyArea(dpy, panel->buffer, panel->window, dwn->gc,
news_region_x, 0, (unsigned int)news_region_width, (unsigned int)panel->height,
news_region_x, 0);
news_region_dirty = false;
}
bool panel_needs_render(void)
{
return panels_dirty;
}
bool panel_news_needs_render(void)
{
return news_region_dirty;
}
void panel_init_systray(void)
{

View File

@ -63,6 +63,83 @@ static int xembed_icons_x = 0;
static void wifi_update_state_internal(void);
static void audio_update_state_internal(void);
static XftDraw *systray_xft_draw = NULL;
static Drawable systray_xft_drawable = None;
static Visual *systray_cached_visual = NULL;
#define SYSTRAY_XFT_COLOR_CACHE_SIZE 8
typedef struct {
unsigned long pixel;
XftColor xft_color;
bool valid;
int lru_counter;
} SystrayCachedColor;
static SystrayCachedColor systray_color_cache[SYSTRAY_XFT_COLOR_CACHE_SIZE];
static int systray_color_lru_counter = 0;
static XftDraw *systray_get_xft_draw(Drawable d)
{
if (systray_xft_drawable == d && systray_xft_draw != NULL) {
return systray_xft_draw;
}
if (systray_xft_draw != NULL) {
XftDrawDestroy(systray_xft_draw);
}
if (systray_cached_visual == NULL) {
systray_cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
systray_xft_draw = XftDrawCreate(dwn->display, d, systray_cached_visual, dwn->colormap);
systray_xft_drawable = d;
return systray_xft_draw;
}
static XftColor *systray_get_cached_color(unsigned long color)
{
if (systray_cached_visual == NULL) {
systray_cached_visual = DefaultVisual(dwn->display, dwn->screen);
}
for (int i = 0; i < SYSTRAY_XFT_COLOR_CACHE_SIZE; i++) {
if (systray_color_cache[i].valid && systray_color_cache[i].pixel == color) {
systray_color_cache[i].lru_counter = ++systray_color_lru_counter;
return &systray_color_cache[i].xft_color;
}
}
int oldest_idx = 0;
int oldest_lru = systray_color_cache[0].lru_counter;
for (int i = 1; i < SYSTRAY_XFT_COLOR_CACHE_SIZE; i++) {
if (!systray_color_cache[i].valid) {
oldest_idx = i;
break;
}
if (systray_color_cache[i].lru_counter < oldest_lru) {
oldest_lru = systray_color_cache[i].lru_counter;
oldest_idx = i;
}
}
if (systray_color_cache[oldest_idx].valid) {
XftColorFree(dwn->display, systray_cached_visual, dwn->colormap,
&systray_color_cache[oldest_idx].xft_color);
}
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, systray_cached_visual, dwn->colormap,
&render_color, &systray_color_cache[oldest_idx].xft_color);
systray_color_cache[oldest_idx].pixel = color;
systray_color_cache[oldest_idx].valid = true;
systray_color_cache[oldest_idx].lru_counter = ++systray_color_lru_counter;
return &systray_color_cache[oldest_idx].xft_color;
}
static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned long color)
{
@ -71,29 +148,11 @@ static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned
}
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
XftDraw *xft_draw = systray_get_xft_draw(d);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
XftColor *xft_color = systray_get_cached_color(color);
XftDrawStringUtf8(xft_draw, xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, strlen(text));
XftColorFree(dwn->display,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}

View File

@ -796,14 +796,39 @@ unsigned long dim_color(unsigned long color, float factor)
return rgb_to_pixel(r, g, b);
}
#define SINE_TABLE_SIZE 256
#define TWO_PI 6.283185307f
static float sine_table[SINE_TABLE_SIZE];
static bool sine_table_initialized = false;
static float fast_sin(float phase)
{
if (!sine_table_initialized) {
for (int i = 0; i < SINE_TABLE_SIZE; i++) {
sine_table[i] = sinf((float)i * TWO_PI / (float)SINE_TABLE_SIZE);
}
sine_table_initialized = true;
}
while (phase < 0.0f) phase += TWO_PI;
while (phase >= TWO_PI) phase -= TWO_PI;
int idx = (int)(phase * (float)SINE_TABLE_SIZE / TWO_PI);
if (idx >= SINE_TABLE_SIZE) idx = SINE_TABLE_SIZE - 1;
if (idx < 0) idx = 0;
return sine_table[idx];
}
unsigned long glow_color(unsigned long base, float phase)
{
int r, g, b;
color_to_rgb(base, &r, &g, &b);
float mod_r = 0.25f * sinf(phase);
float mod_g = 0.25f * sinf(phase + 2.094f);
float mod_b = 0.25f * sinf(phase + 4.189f);
float mod_r = 0.25f * fast_sin(phase);
float mod_g = 0.25f * fast_sin(phase + 2.094f);
float mod_b = 0.25f * fast_sin(phase + 4.189f);
r = r + (int)(r * mod_r);
g = g + (int)(g * mod_g);
@ -824,7 +849,7 @@ unsigned long ambient_glow_bg(unsigned long base, float phase)
int r, g, b;
color_to_rgb(base, &r, &g, &b);
float mod = 0.25f * sinf(phase);
float mod = 0.25f * fast_sin(phase);
r = r + (int)(r * mod);
g = g + (int)(g * mod);
@ -845,9 +870,9 @@ unsigned long ambient_glow_accent(unsigned long base, float phase)
int r, g, b;
color_to_rgb(base, &r, &g, &b);
float mod_r = 0.45f * sinf(phase);
float mod_g = 0.45f * sinf(phase + 2.094f);
float mod_b = 0.45f * sinf(phase + 4.189f);
float mod_r = 0.45f * fast_sin(phase);
float mod_g = 0.45f * fast_sin(phase + 2.094f);
float mod_b = 0.45f * fast_sin(phase + 4.189f);
r = r + (int)(r * mod_r);
g = g + (int)(g * mod_g);