feat: add directional window resizing

feat: support multi-battery monitoring
fix: prevent windows from exceeding layout bounds
refactor: adjust snap constraints for borders and title bars
This commit is contained in:
2026-01-26 20:40:08 +01:00
parent f4af2b1f1e
commit ea12677dac
11 changed files with 211 additions and 34 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+6
View File
@@ -25,6 +25,11 @@
#define MAX_NOTIFICATIONS 32
#define MAX_KEYBINDINGS 64
#define RESIZE_LEFT 1
#define RESIZE_RIGHT 2
#define RESIZE_TOP 4
#define RESIZE_BOTTOM 8
#define DEFAULT_BORDER_WIDTH 0
#define DEFAULT_TITLE_HEIGHT 28
#define DEFAULT_PANEL_HEIGHT 32
@@ -168,6 +173,7 @@ typedef struct {
int drag_orig_x, drag_orig_y;
int drag_orig_w, drag_orig_h;
bool resizing;
int drag_direction;
Client *pending_focus_client;
long pending_focus_time;
+19
View File
@@ -11,6 +11,7 @@
#include <stdbool.h>
#define MAX_TRAY_ICONS 32
#define MAX_BATTERIES 4
#define TRAY_ICON_SIZE 22
#define TRAY_ICON_SPACING 4
@@ -59,6 +60,22 @@ typedef struct {
int time_remaining;
} BatteryState;
typedef struct {
char name[32];
bool present;
bool charging;
int percentage;
unsigned long energy_now;
unsigned long energy_full;
} SingleBattery;
typedef struct {
int count;
SingleBattery batteries[MAX_BATTERIES];
bool any_charging;
int combined_percentage;
} MultiBatteryState;
typedef struct {
Window window;
int x, y;
@@ -70,6 +87,7 @@ typedef struct {
extern WifiState wifi_state;
extern AudioState audio_state;
extern BatteryState battery_state;
extern MultiBatteryState multi_battery_state;
extern VolumeSlider *volume_slider;
void systray_init(void);
@@ -106,6 +124,7 @@ void systray_lock(void);
void systray_unlock(void);
BatteryState systray_get_battery_snapshot(void);
MultiBatteryState systray_get_multi_battery_snapshot(void);
AudioState systray_get_audio_snapshot(void);
void xembed_init(void);
+27 -12
View File
@@ -84,6 +84,7 @@ void layout_arrange_tiling(int workspace)
}
int stack_width = area_width - master_width - 3 * gap;
if (stack_width < 50) stack_width = 50;
int i = 0;
int master_y = area_y + gap;
@@ -120,6 +121,16 @@ void layout_arrange_tiling(int workspace)
stack_y += h + gap;
}
int max_y = area_y + area_height - gap;
int max_x = area_x + area_width - gap;
if (y + h > max_y) {
h = max_y - y;
}
if (x + w > max_x) {
w = max_x - x;
}
int actual_h = h - title_height - 2 * border;
int actual_w = w - 2 * border;
@@ -268,21 +279,25 @@ void layout_apply_snap_constraint(Client *c, int area_x, int area_y,
return;
}
int title_h = (dwn->config && dwn->config->show_decorations) ?
config_get_title_height() : 0;
int border = c->border_width;
int half_w = area_w / 2;
int half_h = area_h / 2;
switch (c->snap.horizontal) {
case SNAP_H_LEFT:
c->x = area_x + gap;
c->width = half_w - gap * 2;
c->x = area_x + gap + border;
c->width = half_w - gap * 2 - 2 * border;
break;
case SNAP_H_RIGHT:
c->x = area_x + half_w + gap;
c->width = half_w - gap * 2;
c->x = area_x + half_w + gap + border;
c->width = half_w - gap * 2 - 2 * border;
break;
case SNAP_H_FULL:
c->x = area_x + gap;
c->width = area_w - gap * 2;
c->x = area_x + gap + border;
c->width = area_w - gap * 2 - 2 * border;
break;
case SNAP_H_NONE:
break;
@@ -290,16 +305,16 @@ void layout_apply_snap_constraint(Client *c, int area_x, int area_y,
switch (c->snap.vertical) {
case SNAP_V_TOP:
c->y = area_y + gap;
c->height = half_h - gap * 2;
c->y = area_y + gap + title_h + border;
c->height = half_h - gap * 2 - title_h - 2 * border;
break;
case SNAP_V_BOTTOM:
c->y = area_y + half_h + gap;
c->height = half_h - gap * 2;
c->y = area_y + half_h + gap + title_h + border;
c->height = half_h - gap * 2 - title_h - 2 * border;
break;
case SNAP_V_FULL:
c->y = area_y + gap;
c->height = area_h - gap * 2;
c->y = area_y + gap + title_h + border;
c->height = area_h - gap * 2 - title_h - 2 * border;
break;
case SNAP_V_NONE:
break;
+32 -5
View File
@@ -467,6 +467,7 @@ static void handle_button_press(XButtonEvent *ev)
dwn->drag_orig_x = c->x;
dwn->drag_orig_y = c->y;
dwn->resizing = false;
dwn->drag_direction = 0;
api_emit_drag_started(c->window, false);
@@ -488,6 +489,7 @@ static void handle_button_press(XButtonEvent *ev)
dwn->drag_orig_w = c->width;
dwn->drag_orig_h = c->height;
dwn->resizing = true;
dwn->drag_direction = direction;
api_emit_drag_started(c->window, true);
@@ -545,11 +547,36 @@ static void handle_motion_notify(XMotionEvent *ev)
int dy = ev->y_root - dwn->drag_start_y;
if (dwn->resizing) {
int new_w = dwn->drag_orig_w + dx;
int new_h = dwn->drag_orig_h + dy;
if (new_w < 50) new_w = 50;
if (new_h < 50) new_h = 50;
client_resize(c, new_w, new_h);
int dir = dwn->drag_direction;
int new_x = dwn->drag_orig_x;
int new_y = dwn->drag_orig_y;
int new_w = dwn->drag_orig_w;
int new_h = dwn->drag_orig_h;
if (dir & RESIZE_LEFT) {
new_x = dwn->drag_orig_x + dx;
new_w = dwn->drag_orig_w - dx;
} else if (dir & RESIZE_RIGHT) {
new_w = dwn->drag_orig_w + dx;
}
if (dir & RESIZE_TOP) {
new_y = dwn->drag_orig_y + dy;
new_h = dwn->drag_orig_h - dy;
} else if (dir & RESIZE_BOTTOM) {
new_h = dwn->drag_orig_h + dy;
}
if (new_w < 50) {
if (dir & RESIZE_LEFT) new_x = dwn->drag_orig_x + dwn->drag_orig_w - 50;
new_w = 50;
}
if (new_h < 50) {
if (dir & RESIZE_TOP) new_y = dwn->drag_orig_y + dwn->drag_orig_h - 50;
new_h = 50;
}
client_move_resize(c, new_x, new_y, new_w, new_h);
} else {
client_move(c, dwn->drag_orig_x + dx, dwn->drag_orig_y + dy);
}
+37 -11
View File
@@ -790,10 +790,23 @@ static int panel_calculate_stats_width(void)
sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min);
total += panel_text_width(buf, len) + WIDGET_SPACING;
BatteryState bat_snap = systray_get_battery_snapshot();
if (bat_snap.present) {
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, bat_snap.percentage);
MultiBatteryState multi_bat = systray_get_multi_battery_snapshot();
if (multi_bat.count > 0) {
const char *bat_icon = multi_bat.any_charging ? "[+]" : "[=]";
if (multi_bat.count == 1) {
len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, multi_bat.combined_percentage);
} else {
int offset = snprintf(buf, sizeof(buf), "%s%d%% (", bat_icon, multi_bat.combined_percentage);
for (int i = 0; i < multi_bat.count && offset < (int)sizeof(buf) - 10; i++) {
if (i > 0) {
offset += snprintf(buf + offset, sizeof(buf) - offset, " ");
}
offset += snprintf(buf + offset, sizeof(buf) - offset, "B%d:%d%%",
i + 1, multi_bat.batteries[i].percentage);
}
snprintf(buf + offset, sizeof(buf) - offset, ")");
len = strlen(buf);
}
total += panel_text_width(buf, len) + WIDGET_SPACING;
}
@@ -859,19 +872,32 @@ static void panel_render_system_stats(Panel *panel, int x, int *width)
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, load_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
BatteryState bat_snap = systray_get_battery_snapshot();
if (bat_snap.present) {
MultiBatteryState multi_bat = systray_get_multi_battery_snapshot();
if (multi_bat.count > 0) {
unsigned long bat_color = colors->panel_fg;
if (bat_snap.percentage <= 20 && !bat_snap.charging) {
if (multi_bat.combined_percentage <= 20 && !multi_bat.any_charging) {
bat_color = colors->workspace_urgent;
} else if (bat_snap.percentage <= 40 && !bat_snap.charging) {
} else if (multi_bat.combined_percentage <= 40 && !multi_bat.any_charging) {
bat_color = 0xFFA500;
} else if (bat_snap.charging) {
} else if (multi_bat.any_charging) {
bat_color = colors->workspace_active;
}
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, bat_snap.percentage);
const char *bat_icon = multi_bat.any_charging ? "[+]" : "[=]";
if (multi_bat.count == 1) {
len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, multi_bat.combined_percentage);
} else {
int offset = snprintf(stats_buf, sizeof(stats_buf), "%s%d%% (", bat_icon, multi_bat.combined_percentage);
for (int i = 0; i < multi_bat.count && offset < (int)sizeof(stats_buf) - 10; i++) {
if (i > 0) {
offset += snprintf(stats_buf + offset, sizeof(stats_buf) - offset, " ");
}
offset += snprintf(stats_buf + offset, sizeof(stats_buf) - offset, "B%d:%d%%",
i + 1, multi_bat.batteries[i].percentage);
}
snprintf(stats_buf + offset, sizeof(stats_buf) - offset, ")");
len = strlen(stats_buf);
}
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, bat_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
}
+89 -5
View File
@@ -43,6 +43,7 @@
WifiState wifi_state = {0};
AudioState audio_state = {0};
BatteryState battery_state = {0};
MultiBatteryState multi_battery_state = {0};
VolumeSlider *volume_slider = NULL;
TrayIcon tray_icons[MAX_TRAY_ICONS];
@@ -183,6 +184,7 @@ void battery_update_state(void)
char value[64];
FILE *fp;
memset(&multi_battery_state, 0, sizeof(multi_battery_state));
battery_state.present = false;
battery_state.charging = false;
battery_state.percentage = 0;
@@ -192,24 +194,75 @@ void battery_update_state(void)
return;
}
unsigned long total_energy_now = 0;
unsigned long total_energy_full = 0;
bool any_charging = false;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
if (multi_battery_state.count >= MAX_BATTERIES) {
break;
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", entry->d_name);
fp = fopen(path, "r");
if (fp != NULL) {
if (fgets(value, sizeof(value), fp) != NULL) {
value[strcspn(value, "\n")] = '\0';
if (strcmp(value, "Battery") == 0) {
battery_state.present = true;
SingleBattery *bat = &multi_battery_state.batteries[multi_battery_state.count];
bat->present = true;
size_t name_len = strlen(entry->d_name);
if (name_len >= sizeof(bat->name)) {
name_len = sizeof(bat->name) - 1;
}
memcpy(bat->name, entry->d_name, name_len);
bat->name[name_len] = '\0';
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/energy_now", entry->d_name);
FILE *energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_now = strtoul(value, NULL, 10);
}
fclose(energy_fp);
} else {
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/charge_now", entry->d_name);
energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_now = strtoul(value, NULL, 10);
}
fclose(energy_fp);
}
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/energy_full", entry->d_name);
energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_full = strtoul(value, NULL, 10);
}
fclose(energy_fp);
} else {
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/charge_full", entry->d_name);
energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_full = strtoul(value, NULL, 10);
}
fclose(energy_fp);
}
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", entry->d_name);
FILE *cap_fp = fopen(path, "r");
if (cap_fp != NULL) {
if (fgets(value, sizeof(value), cap_fp) != NULL) {
battery_state.percentage = atoi(value);
bat->percentage = atoi(value);
}
fclose(cap_fp);
}
@@ -219,20 +272,42 @@ void battery_update_state(void)
if (status_fp != NULL) {
if (fgets(value, sizeof(value), status_fp) != NULL) {
value[strcspn(value, "\n")] = '\0';
battery_state.charging = (strcmp(value, "Charging") == 0 ||
bat->charging = (strcmp(value, "Charging") == 0 ||
strcmp(value, "Full") == 0);
if (bat->charging) {
any_charging = true;
}
}
fclose(status_fp);
}
fclose(fp);
break;
total_energy_now += bat->energy_now;
total_energy_full += bat->energy_full;
multi_battery_state.count++;
}
}
fclose(fp);
}
}
closedir(dir);
multi_battery_state.any_charging = any_charging;
if (total_energy_full > 0) {
multi_battery_state.combined_percentage = (int)((total_energy_now * 100) / total_energy_full);
} else if (multi_battery_state.count > 0) {
int total_pct = 0;
for (int i = 0; i < multi_battery_state.count; i++) {
total_pct += multi_battery_state.batteries[i].percentage;
}
multi_battery_state.combined_percentage = total_pct / multi_battery_state.count;
}
if (multi_battery_state.count > 0) {
battery_state.present = true;
battery_state.charging = any_charging;
battery_state.percentage = multi_battery_state.combined_percentage;
}
}
const char *battery_get_icon(void)
@@ -707,6 +782,15 @@ BatteryState systray_get_battery_snapshot(void)
return snapshot;
}
MultiBatteryState systray_get_multi_battery_snapshot(void)
{
MultiBatteryState snapshot;
pthread_mutex_lock(&state_mutex);
snapshot = multi_battery_state;
pthread_mutex_unlock(&state_mutex);
return snapshot;
}
AudioState systray_get_audio_snapshot(void)
{
AudioState snapshot;