Add modem/networks DWM plugins, Dual-SIM & PIN/PUK support, built-in dmenu views, and game UX fixes

This commit is contained in:
Luxferre
2026-10-24 18:59:48 +03:00
parent 668a4da11d
commit e549aaf413
63 changed files with 4781 additions and 426 deletions
+20
View File
@@ -35,8 +35,13 @@ static int is_dmenu_builtin_arg(const char *arg) {
return strcmp(arg, "sys") == 0 ||
strcmp(arg, "ctx") == 0 ||
strcmp(arg, "applications") == 0 ||
strcmp(arg, "games") == 0 ||
strcmp(arg, "scripts") == 0 ||
strcmp(arg, "modem") == 0 ||
strcmp(arg, "audio") == 0 ||
strcmp(arg, "notifications") == 0 ||
strcmp(arg, "wm") == 0 ||
strcmp(arg, "timezone") == 0 ||
strcmp(arg, "config") == 0 ||
strcmp(arg, "power") == 0 ||
strcmp(arg, "st") == 0 ||
@@ -206,6 +211,14 @@ static int try_send_plugin_fifo(const char *plugin_name, int sub_argc, char **su
len = snprintf(cmd, sizeof(cmd), "OPEN thread:%s\n", num);
} else if (sub_argc >= 2 && sub_argv[1] && sub_argv[1][0]) {
len = snprintf(cmd, sizeof(cmd), "OPEN %s\n", sub_argv[1]);
} else if (sub_argc >= 1 && sub_argv[0]) {
const char *b0 = strrchr(sub_argv[0], '/');
b0 = b0 ? (b0 + 1) : sub_argv[0];
if (strcasecmp(b0, "xbluetooth") == 0 || strcasecmp(b0, "bluetooth") == 0) {
len = snprintf(cmd, sizeof(cmd), "OPEN bluetooth\n");
} else {
len = snprintf(cmd, sizeof(cmd), "OPEN\n");
}
} else {
len = snprintf(cmd, sizeof(cmd), "OPEN\n");
}
@@ -254,6 +267,13 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) {
if (strcasecmp(base, "xthemes") == 0 || strcasecmp(base, "themes") == 0) {
return "themes";
}
if (strcasecmp(base, "xmodem") == 0 || strcasecmp(base, "modem") == 0) {
return "modem";
}
if (strcasecmp(base, "xnetworks") == 0 || strcasecmp(base, "networks") == 0 ||
strcasecmp(base, "xbluetooth") == 0 || strcasecmp(base, "bluetooth") == 0) {
return "networks";
}
return NULL;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+35
View File
@@ -78,6 +78,17 @@ constexpr const char* kIconSoundOff = "\xef\x91\xa6"; // U+F466 mute
constexpr const char* kIconMicOn = "\xf3\xb0\x8d\xac"; // U+F036C mic on
constexpr const char* kIconMicOff = "\xf3\xb0\x8d\xad"; // U+F036D mic off
constexpr const char* kIconRecord = "\xef\x84\x91"; // U+F111 circle / record dot
constexpr const char* kIconLock = "\xef\x80\xa3"; // U+F023 lock
constexpr const char* kIconUnlock = "\xef\x82\x9c"; // U+F09C unlock
constexpr const char* kIconSignal = "\xef\x80\x92"; // U+F012 signal bars
constexpr const char* kIconWifi = "\xef\x87\xab"; // U+F1EB wifi
constexpr const char* kIconBluetooth = "\xef\x8a\x94"; // U+F294 bluetooth
constexpr const char* kIconGlobe = "\xef\x82\xac"; // U+F0AC globe / network
constexpr const char* kIconSliders = "\xef\x87\x9e"; // U+F1DE sliders / config
constexpr const char* kIconToggleOn = "\xef\x88\x85"; // U+F205 toggle on
constexpr const char* kIconToggleOff = "\xef\x88\x84"; // U+F204 toggle off
constexpr const char* kIconKey = "\xef\x82\x84"; // U+F084 key / PIN
constexpr const char* kIconSim = "\xef\x9f\x84"; // U+F7C4 sim card / chip
inline int64_t nowMs() {
struct timespec ts {};
@@ -333,6 +344,26 @@ inline void runAsyncCmd(const std::string& cmd) {
}
}
inline std::string runCaptureCmd(const std::string& cmd, int* exitCodeOut = nullptr) {
FILE* fp = ::popen(cmd.c_str(), "r");
if (!fp) {
if (exitCodeOut) {
*exitCodeOut = -1;
}
return "";
}
std::string out;
char buf[512];
while (std::fgets(buf, sizeof(buf), fp) != nullptr) {
out += buf;
}
int status = ::pclose(fp);
if (exitCodeOut) {
*exitCodeOut = status;
}
return out;
}
inline void runAsyncArgv(const char* prog,
const char* arg1 = nullptr,
const char* arg2 = nullptr,
@@ -1013,6 +1044,8 @@ inline void closeOtherPlugins(const char* selfPluginName) {
{"xfiles.pid", "files"},
{"xcamera.pid", "camera"},
{"xthemes.pid", "themes"},
{"xmodem.pid", "modem"},
{"xnetworks.pid", "networks"},
};
for (const auto& p : kAllPlugins) {
if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) {
@@ -1039,6 +1072,8 @@ inline void broadcastThemeReload(const char* selfPluginName = nullptr) {
"files",
"camera",
"themes",
"modem",
"networks",
};
for (const char* name : kPlugins) {
if (selfPluginName && std::strcmp(name, selfPluginName) == 0) {
+230 -142
View File
@@ -3,28 +3,32 @@
// bitmap font so it has no font-file dependency.
//
// Controls (Nokia keypad + D-pad):
// 2 / Up -> turn up 8 / Down -> turn down
// 2 / Up -> turn up 8 / Down -> turn down
// 4 / Left -> turn left 6 / Right -> turn right
// 5 / OK / Enter -> pause / restart (after game over)
// # / * / P -> pause / resume
// Back / Esc -> quit
//
// Build: g++ -O2 snake.cpp -lX11 -o snake
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#ifndef _EVDEVK
#define _EVDEVK(_v) (0x10081000 + (_v))
#endif
#include <X11/Xatom.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cstdint>
#include <cerrno>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <time.h>
#include <sys/timerfd.h>
#include <cstdint>
#include <cerrno>
static const int TOPBAR = 24;
static const int COLS = 24;
@@ -48,21 +52,22 @@ static const Glyph GLYPHS[] = {
{'G', {0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01111}},
{'H', {0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
{'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'K', {0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001}},
{'L', {0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111}},
{'M', {0b10001,0b11011,0b10101,0b10101,0b10001,0b10001,0b10001}},
{'N', {0b10001,0b10001,0b11001,0b10101,0b10011,0b10001,0b10001}},
{'O', {0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'P', {0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}},
{'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}},
{'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}},
{'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}},
{'X', {0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001}},
{'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'Z', {0b11111,0b00010,0b00100,0b01000,0b10000,0b10000,0b11111}},
{'0', {0b01110,0b10011,0b10101,0b10101,0b11001,0b10001,0b01110}},
{'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}},
@@ -81,11 +86,19 @@ static const Glyph GLYPHS[] = {
{'=', {0b00000,0b00000,0b11111,0b00000,0b11111,0b00000,0b00000}},
};
static const int NGLYPH = int(sizeof(GLYPHS) / sizeof(GLYPHS[0]));
static unsigned char GLYPH_LUT[128][7];
static const unsigned char* glyph(char c) {
for (int i = 0; i < NGLYPH; i++)
if (GLYPHS[i].ch == c) return GLYPHS[i].r;
return GLYPHS[0].r; // space
static void init_glyph_lut() {
memset(GLYPH_LUT, 0, sizeof(GLYPH_LUT));
for (int i = 0; i < NGLYPH; i++) {
unsigned char idx = (unsigned char)GLYPHS[i].ch;
if (idx < 128) memcpy(GLYPH_LUT[idx], GLYPHS[i].r, 7);
}
}
static inline const unsigned char* glyph(char c) {
unsigned char idx = (unsigned char)c;
return (idx < 128) ? GLYPH_LUT[idx] : GLYPH_LUT[(unsigned char)' '];
}
// ---- X11 state ----
@@ -96,6 +109,7 @@ static Pixmap back;
static int screen;
static int cell, gx0, gy0, gw, gh;
static int winW = 240, winH = 296; // overridden at runtime from the actual screen
static Atom wmDeleteAtom = None;
// ---- colors (Nokia green monochrome) ----
static unsigned long C_BG, C_FG, C_HEAD, C_FOOD, C_DIM;
@@ -114,10 +128,18 @@ static int best = 0;
static int speed = 100; // ms per step (derived from pps below)
static int pps = 90; // base speed px/sec; +1 px/sec per 5 food eaten
static bool running = true;
static unsigned long now_ms() {
static bool dirty = true;
static bool resyncTimer = false;
static int64_t lastStepNs = 0;
static int64_t now_ns() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
return (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec;
}
static inline int64_t step_ns() {
return (int64_t)speed * 1000000LL;
}
// Derive the per-step delay (ms) from the target pixels/second. The snake
@@ -173,30 +195,33 @@ static void reset_game() {
update_speed();
place_food();
state = PLAYING;
lastStepNs = now_ns();
resyncTimer = true;
dirty = true;
}
// Queue a pending direction change; rapid key presses are stored so none are lost.
// Filters reversals and duplicates against the last queued (or current) direction.
static void tq_push(int dx, int dy) {
static bool tq_push(int dx, int dy) {
int refx = (tqn > 0) ? tqx[tqn - 1] : dirx;
int refy = (tqn > 0) ? tqy[tqn - 1] : diry;
if (dx == -refx && dy == -refy) return; // reversal
if (dx == refx && dy == refy) return; // duplicate
if (dx == -refx && dy == -refy) return false; // reversal
if (dx == refx && dy == refy) return false; // duplicate (including X11 key repeat)
if (tqn < 2) {
tqx[tqn] = dx;
tqy[tqn] = dy;
tqn++;
return true;
}
return false;
}
static void step() {
// apply queued turn (no reversing); rapid presses are not lost
if (tqn > 0) {
int dx = tqx[0], dy = tqy[0];
// shift queue
tqx[0] = tqx[1]; tqy[0] = tqy[1];
tqn--;
// apply if not a reversal
if (!(dx == -dirx && dy == -diry) && !(dx == 0 && dy == 0)) {
dirx = dx; diry = dy;
}
@@ -208,11 +233,14 @@ static void step() {
hy = (hy + ROWS) % ROWS;
// self collision (ignore current tail cell since it will move, unless growing)
for (int i = 0; i < snakelen - 1; i++) {
if (snake[i][0] == hx && snake[i][1] == hy) { state = GAMEOVER; return; }
if (snake[i][0] == hx && snake[i][1] == hy) {
state = GAMEOVER;
dirty = true;
return;
}
}
bool ate = (hx == foodx && hy == foody);
if (!ate) {
// shift body toward tail
for (int i = 0; i < snakelen - 1; i++) {
snake[i][0] = snake[i + 1][0];
snake[i][1] = snake[i + 1][1];
@@ -226,109 +254,159 @@ static void step() {
score += 10;
if (score > best) best = score;
int food = score / 10; // food items eaten so far
pps = 90 + food / 5; // +1 px/sec per 5 food eaten (progressive difficulty)
pps = 90 + food / 5; // +1 px/sec per 5 food eaten (progressive difficulty)
update_speed();
place_food();
}
lastStepNs = now_ns();
dirty = true;
}
// Batched text rendering: coalesces horizontal pixel runs in each glyph row
// and emits a single XFillRectangles call per string.
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
XRectangle rects[512];
int nrects = 0;
int cx = x;
XSetForeground(dpy, gc, col);
for (const char* p = s; *p; p++) {
const unsigned char* g = glyph(*p);
for (int r = 0; r < 7; r++) {
unsigned char row = g[r];
for (int b = 0; b < 5; b++)
if (row & (1 << (4 - b)))
XFillRectangle(dpy, back, gc, cx + b * scale, y + r * scale, scale, scale);
if (!row) continue;
int b = 0;
while (b < 5) {
if (row & (1 << (4 - b))) {
int start = b;
while (b < 5 && (row & (1 << (4 - b)))) b++;
if (nrects < (int)(sizeof(rects) / sizeof(rects[0]))) {
rects[nrects].x = (short)(cx + start * scale);
rects[nrects].y = (short)(y + r * scale);
rects[nrects].width = (unsigned short)((b - start) * scale);
rects[nrects].height = (unsigned short)scale;
nrects++;
}
} else {
b++;
}
}
}
cx += 6 * scale;
}
if (nrects > 0) {
XSetForeground(dpy, gc, col);
XFillRectangles(dpy, back, gc, rects, nrects);
}
}
static void render() {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
// border framing the whole window (adapts to the actual screen size)
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// food
if (foodx >= 0) {
XSetForeground(dpy, gc, C_FOOD);
XFillRectangle(dpy, back, gc, gx0 + foodx * cell, gy0 + foody * cell, cell, cell);
}
// snake (batched for speed)
XRectangle bodyRects[COLS * ROWS];
int nbody = 0;
for (int i = 0; i < snakelen - 1; i++) {
bodyRects[nbody].x = gx0 + snake[i][0] * cell;
bodyRects[nbody].y = gy0 + snake[i][1] * cell;
bodyRects[nbody].width = cell;
bodyRects[nbody].height = cell;
nbody++;
}
XSetForeground(dpy, gc, C_FG);
XFillRectangles(dpy, back, gc, bodyRects, nbody);
// head
XSetForeground(dpy, gc, C_HEAD);
XFillRectangle(dpy, back, gc, gx0 + snake[snakelen - 1][0] * cell, gy0 + snake[snakelen - 1][1] * cell, cell, cell);
// bright frame around the playfield so its boundaries are unmistakable
XSetForeground(dpy, gc, C_FG);
XDrawRectangle(dpy, back, gc, gx0, gy0, gw, gh);
if (state == PLAYING) {
// border framing the whole window (adapts to the actual screen size)
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// food
if (foodx >= 0) {
XSetForeground(dpy, gc, C_FOOD);
XFillRectangle(dpy, back, gc, gx0 + foodx * cell, gy0 + foody * cell, cell, cell);
}
// snake body (batched for speed)
XRectangle bodyRects[COLS * ROWS];
int nbody = 0;
for (int i = 0; i < snakelen - 1; i++) {
bodyRects[nbody].x = (short)(gx0 + snake[i][0] * cell);
bodyRects[nbody].y = (short)(gy0 + snake[i][1] * cell);
bodyRects[nbody].width = (unsigned short)cell;
bodyRects[nbody].height = (unsigned short)cell;
nbody++;
}
if (nbody > 0) {
XSetForeground(dpy, gc, C_FG);
XFillRectangles(dpy, back, gc, bodyRects, nbody);
}
// head
XSetForeground(dpy, gc, C_HEAD);
XFillRectangle(dpy, back, gc, gx0 + snake[snakelen - 1][0] * cell,
gy0 + snake[snakelen - 1][1] * cell, cell, cell);
// bright frame around the playfield (1px outside the cell grid so row 0 / col 0 are not overlapped)
XSetForeground(dpy, gc, C_FG);
XDrawRectangle(dpy, back, gc, gx0 - 1, gy0 - 1, gw + 1, gh + 1);
// score / best sit in the margin between the window frame (outer) and the
// playfield frame (inner), vertically centred there so the playfield boundary
// stays clear
int scoreY = (2 + gy0) / 2 - 3;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(gx0, scoreY, 1, C_DIM, buf);
if (best > 0) {
snprintf(buf, sizeof(buf), "BEST %d", best);
draw_text(gx0 + gw - 11 * 6, scoreY, 1, C_DIM, buf);
}
if (state == PAUSED || state == GAMEOVER) {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
}
if (state == PAUSED) {
// score / best sit in the margin between the window frame (outer) and the
// playfield frame (inner), vertically centred there
int scoreY = (2 + gy0 - 1) / 2 - 3;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(gx0, scoreY, 1, C_DIM, buf);
if (best > 0) {
snprintf(buf, sizeof(buf), "BEST %d", best);
int bw = (int)strlen(buf) * 6;
draw_text(gx0 + gw - bw, scoreY, 1, C_DIM, buf);
}
} else if (state == PAUSED) {
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
draw_text(winW / 2 - 5 * 6, winH / 2 - 7 * 2, 2, C_FG, "PAUSE");
draw_text(winW / 2 - 4 * 6, winH / 2 + 7 * 2, 1, C_DIM, "5 RESUME");
} else if (state == GAMEOVER) {
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
draw_text(winW / 2 - 9 * 6, winH / 2 - 14, 2, C_FG, "GAME OVER");
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(winW / 2 - 5 * 6, winH / 2 + 4, 1, C_DIM, buf);
draw_text(winW / 2 - (int)strlen(buf) * 3, winH / 2 + 4, 1, C_DIM, buf);
draw_text(winW / 2 - 5 * 6, winH / 2 + 4 + 9, 1, C_DIM, "5 NEW GAME");
}
XCopyArea(dpy, back, win, gc, 0, 0, winW, winH, 0, 0);
XFlush(dpy);
dirty = false;
}
static void handle_turn(int dx, int dy) {
if (state != PLAYING) return;
if (tq_push(dx, dy)) {
// If this is the first queued turn and at least half a step interval has
// already elapsed since the previous step, execute the turn immediately
// and re-anchor the step timer so turns feel instantaneous (< 1ms latency)
// without allowing rapid key mashing or U-turns to jump 2 cells at once.
if (tqn == 1 && (now_ns() - lastStepNs) >= (step_ns() / 2)) {
step();
resyncTimer = true;
}
}
}
static void handle_key(KeySym ks) {
switch (ks) {
case XK_Up: case XK_w: case XK_KP_Up: case XK_2:
tq_push(0, -1); break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x:
tq_push(0, 1); break;
case XK_Left: case XK_a: case XK_KP_Left: case XK_4:
tq_push(-1, 0); break;
case XK_Right: case XK_d: case XK_KP_Right: case XK_6:
tq_push(1, 0); break;
case XK_5: case XK_Return: case XK_KP_Enter: case XK_space: case XK_p:
if (state == PLAYING) state = PAUSED;
else if (state == PAUSED) { state = PLAYING; }
case XK_Up: case XK_w: case XK_KP_Up: case XK_2: case XK_KP_2:
handle_turn(0, -1);
break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_KP_8: case XK_x:
handle_turn(0, 1);
break;
case XK_Left: case XK_a: case XK_KP_Left: case XK_4: case XK_KP_4:
handle_turn(-1, 0);
break;
case XK_Right: case XK_d: case XK_KP_Right: case XK_6: case XK_KP_6:
handle_turn(1, 0);
break;
case XK_p: case XK_numbersign: case XK_slash: case XK_KP_Divide:
case XK_asterisk: case XK_KP_Multiply:
if (state == PLAYING) { state = PAUSED; dirty = true; }
else if (state == PAUSED) { state = PLAYING; lastStepNs = now_ns(); resyncTimer = true; dirty = true; }
break;
case XK_5: case XK_KP_5: case XK_Return: case XK_KP_Enter: case XK_space:
if (state == PLAYING) { state = PAUSED; dirty = true; }
else if (state == PAUSED) { state = PLAYING; lastStepNs = now_ns(); resyncTimer = true; dirty = true; }
else if (state == GAMEOVER) reset_game();
break;
case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End:
case XK_Menu:
case XK_Menu: case XF86XK_Back: case XF86XK_KbdLcdMenu2:
running = false;
break;
default:
if (state == GAMEOVER) reset_game();
break;
}
}
@@ -349,10 +427,15 @@ static void setup_window() {
if (winW < 96) winW = 96;
if (winH < 96) winH = 96;
win = XCreateWindow(dpy, RootWindow(dpy, screen), bw, TOPBAR + bw, winW, winH, 0,
CopyFromParent, InputOutput, CopyFromParent, 0, nullptr);
CopyFromParent, InputOutput, CopyFromParent, 0, nullptr);
XStoreName(dpy, win, "Snake");
XClassHint* ch = XAllocClassHint();
if (ch) { ch->res_name = (char*)"snake"; ch->res_class = (char*)"snake"; XSetClassHint(dpy, win, ch); XFree(ch); }
if (ch) {
ch->res_name = (char*)"snake";
ch->res_class = (char*)"snake";
XSetClassHint(dpy, win, ch);
XFree(ch);
}
// float as a dialog so dwm doesn't tile it
Atom wtype = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
@@ -366,10 +449,14 @@ static void setup_window() {
sh.min_height = sh.max_height = winH;
XSetWMNormalHints(dpy, win, &sh);
Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, win, &wm_del, 1);
wmDeleteAtom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, win, &wmDeleteAtom, 1);
gc = XCreateGC(dpy, win, 0, nullptr);
// CRITICAL: disable GraphicsExposures so XCopyArea does not emit a NoExpose
// event on every frame (which previously woke select() in an infinite 100%
// CPU redraw storm).
XSetGraphicsExposures(dpy, gc, False);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
C_BG = BlackPixel(dpy, screen);
@@ -379,73 +466,74 @@ static void setup_window() {
C_FOOD = 0x66ff66;
C_DIM = 0x2f9f2f;
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask);
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask | FocusChangeMask);
XMapWindow(dpy, win);
compute_grid();
}
static void handle_event(XEvent& e) {
if (e.type == ClientMessage) {
Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
if ((Atom)e.xclient.data.l[0] == wm_del) running = false;
if ((Atom)e.xclient.data.l[0] == wmDeleteAtom) running = false;
} else if (e.type == ConfigureNotify) {
if (e.xconfigure.width != winW || e.xconfigure.height != winH) {
winW = e.xconfigure.width; winH = e.xconfigure.height;
winW = e.xconfigure.width;
winH = e.xconfigure.height;
if (back) XFreePixmap(dpy, back);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
compute_grid();
dirty = true;
}
} else if (e.type == Expose) {
render();
if (e.xexpose.count == 0) dirty = true;
} else if (e.type == FocusOut) {
if (e.xfocus.mode == NotifyNormal && state == PLAYING) {
state = PAUSED;
dirty = true;
}
} else if (e.type == KeyPress) {
char buf[8];
KeySym ks = 0;
XLookupString(&e.xkey, buf, sizeof(buf), &ks, nullptr);
if (ks == NoSymbol) ks = XLookupKeysym(&e.xkey, 0);
if (ks == XK_q) { running = false; return; }
if (ks == XK_q || ks == XK_Q) { running = false; return; }
handle_key(ks);
}
}
int main() {
srand((unsigned)time(nullptr) ^ (unsigned)getpid());
init_glyph_lut();
setup_window();
reset_game();
render();
int xfd = ConnectionNumber(dpy);
// Precise, drift-free stepping via a kernel high-resolution timer (timerfd).
// The timer fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are not
// quantized to the kernel tick the way a select() timeout is. A select()
// timeout shorter than one tick gets rounded UP to a whole tick, which made
// the final few milliseconds of each wait overshoot by up to ~20ms and showed
// up as speed jitter. Input still arrives on the X connection fd, so we
// select() on BOTH fds and react to either immediately -- there is no
// busy-spin, so the X server stays free and the controls stay responsive.
int tfd = -1;
#if defined(__linux__)
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
#endif
unsigned long nextStep = now_ms() + speed;
int64_t nextStep = now_ns() + step_ns();
State prevState = PLAYING;
resyncTimer = false;
auto arm_timer = [&](unsigned long deadline_ms) {
auto arm_timer = [&](int64_t deadline_ns) {
if (tfd < 0) return;
struct itimerspec its;
its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; // one-shot
its.it_value.tv_sec = (time_t)(deadline_ms / 1000);
its.it_value.tv_nsec = (long)((deadline_ms % 1000) * 1000000UL);
if (its.it_value.tv_nsec >= 1000000000L) {
its.it_value.tv_sec += 1; its.it_value.tv_nsec -= 1000000000L;
}
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 0;
its.it_value.tv_sec = (time_t)(deadline_ns / 1000000000LL);
its.it_value.tv_nsec = (long)(deadline_ns % 1000000000LL);
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, nullptr);
};
auto disarm_timer = [&]() {
if (tfd < 0) return;
struct itimerspec its;
memset(&its, 0, sizeof(its));
timerfd_settime(tfd, 0, &its, nullptr);
};
if (tfd >= 0) arm_timer(nextStep);
arm_timer(nextStep);
while (running) {
fd_set fds;
@@ -455,18 +543,15 @@ int main() {
struct timeval tvbuf, *tvp = nullptr;
if (tfd >= 0) {
// While playing, also wait on the timer fd. While paused / game over the
// timer is disarmed, so we block on input only (resume stays instant).
if (state == PLAYING) {
FD_SET(tfd, &fds);
if (tfd + 1 > nfds) nfds = tfd + 1;
}
} else {
// Fallback (timerfd unavailable): bounded select so input stays live.
long sleepMs = (state == PLAYING) ? (long)nextStep - (long)now_ms() : 30;
if (sleepMs < 0) sleepMs = 0;
tvbuf.tv_sec = sleepMs / 1000;
tvbuf.tv_usec = (sleepMs % 1000) * 1000;
} else if (state == PLAYING) {
int64_t remNs = nextStep - now_ns();
if (remNs < 0) remNs = 0;
tvbuf.tv_sec = (time_t)(remNs / 1000000000LL);
tvbuf.tv_usec = (suseconds_t)((remNs % 1000000000LL) / 1000LL);
tvp = &tvbuf;
}
@@ -484,42 +569,45 @@ int main() {
if (!running) break;
}
// 2) (Re)start play cleanly: resync the clock on resume / restart / launch,
// and stop the timer whenever we leave the PLAYING state.
if (state != prevState) {
if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); }
else if (tfd >= 0) disarm_timer();
// 2) Resync timer on state transitions or immediate turn steps.
if (state != prevState || resyncTimer) {
if (state == PLAYING) {
nextStep = now_ns() + step_ns();
arm_timer(nextStep);
} else {
disarm_timer();
}
prevState = state;
}
// 3) Advance the simulation exactly once per timer expiration (or, in the
// fallback path, whenever the deadline is reached). If we ever fell more
// than one interval behind (e.g. the process was frozen for a screenshot)
// we drop the backlog instead of racing to catch up.
if (state == PLAYING) {
resyncTimer = false;
} else if (state == PLAYING) {
// 3) Advance simulation on scheduled timer expiration.
bool due = false;
if (tfd >= 0) {
if (FD_ISSET(tfd, &fds)) {
uint64_t expirations = 0;
ssize_t n = read(tfd, &expirations, sizeof(expirations));
(void)n; (void)expirations;
(void)n;
due = true;
}
} else {
if ((long)(now_ms() - nextStep) >= 0) due = true;
if (now_ns() >= nextStep) due = true;
}
if (due) {
step();
nextStep += speed; // advance by exactly one interval (no drift)
unsigned long now = now_ms();
if ((long)(now - nextStep) > (long)speed) nextStep = now + speed;
if (tfd >= 0 && state == PLAYING) arm_timer(nextStep);
if (state == PLAYING) {
nextStep += step_ns();
int64_t now = now_ns();
if (now - nextStep > step_ns()) nextStep = now + step_ns();
arm_timer(nextStep);
} else {
disarm_timer();
prevState = state;
}
}
}
// 4) Repaint on every loop turn (input or step) so the display never
// appears frozen and key feedback is immediate.
render();
// 4) Repaint only when visual state actually changed.
if (dirty) render();
}
if (tfd >= 0) close(tfd);
+327 -199
View File
@@ -4,28 +4,31 @@
//
// Controls (Nokia keypad + D-pad):
// 4 / Left -> move left 6 / Right -> move right
// 2 / Up -> rotate CW 8 / Down -> soft drop
// 0 / Space -> hard drop # / * / P -> pause
// 5 / OK -> resume / new game
// Back / Esc -> quit
// 2 / Up -> rotate CW 8 / Down -> soft drop
// 0 / 5 / Space -> hard drop # / * / P -> pause
// 5 / OK -> resume / new game
// Back / Esc -> quit
//
// Build: g++ -O2 tetris.cpp -lX11 -o tetris
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#ifndef _EVDEVK
#define _EVDEVK(_v) (0x10081000 + (_v))
#endif
#include <X11/Xatom.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cstdint>
#include <cerrno>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <time.h>
#include <sys/timerfd.h>
#include <cstdint>
#include <cerrno>
static const int TOPBAR = 24;
static const int PCOLS = 10;
@@ -44,27 +47,28 @@ static const Glyph GLYPHS[] = {
{'A', {0b01110,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
{'B', {0b11110,0b10001,0b10001,0b11110,0b10001,0b10001,0b11110}},
{'C', {0b01110,0b10001,0b10000,0b10000,0b10000,0b10001,0b01110}},
{'D', {0b11110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'E', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b11111}},
{'F', {0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b10000}},
{'G', {0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01111}},
{'H', {0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001}},
{'I', {0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'K', {0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001}},
{'L', {0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111}},
{'M', {0b10001,0b11011,0b10101,0b10101,0b10001,0b10001,0b10001}},
{'N', {0b10001,0b10001,0b11001,0b10101,0b10011,0b10001,0b10001}},
{'O', {0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'P', {0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'R', {0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001}},
{'S', {0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110}},
{'T', {0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'V', {0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100}},
{'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}},
{'X', {0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001}},
{'Y', {0b10001,0b10001,0b01010,0b00100,0b00100,0b00100,0b00100}},
{'D', {0b11110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'J', {0b00010,0b00010,0b00010,0b00010,0b10010,0b10010,0b01100}},
{'K', {0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001}},
{'Q', {0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101}},
{'U', {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110}},
{'W', {0b10001,0b10001,0b10001,0b10101,0b10101,0b11011,0b10001}},
{'Z', {0b11111,0b00010,0b00100,0b01000,0b10000,0b10000,0b11111}},
{'0', {0b01110,0b10011,0b10101,0b10101,0b11001,0b10001,0b01110}},
{'1', {0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110}},
@@ -83,10 +87,19 @@ static const Glyph GLYPHS[] = {
{'=', {0b00000,0b00000,0b11111,0b00000,0b11111,0b00000,0b00000}},
};
static const int NGLYPH = int(sizeof(GLYPHS) / sizeof(GLYPHS[0]));
static const unsigned char* glyph(char c) {
for (int i = 0; i < NGLYPH; i++)
if (GLYPHS[i].ch == c) return GLYPHS[i].r;
return GLYPHS[0].r;
static unsigned char GLYPH_LUT[128][7];
static void init_glyph_lut() {
memset(GLYPH_LUT, 0, sizeof(GLYPH_LUT));
for (int i = 0; i < NGLYPH; i++) {
unsigned char idx = (unsigned char)GLYPHS[i].ch;
if (idx < 128) memcpy(GLYPH_LUT[idx], GLYPHS[i].r, 7);
}
}
static inline const unsigned char* glyph(char c) {
unsigned char idx = (unsigned char)c;
return (idx < 128) ? GLYPH_LUT[idx] : GLYPH_LUT[(unsigned char)' '];
}
// ---- X11 state ----
@@ -96,6 +109,7 @@ static GC gc;
static Pixmap back;
static int screen;
static int winW = 240, winH = 296; // overridden at runtime from the actual screen
static Atom wmDeleteAtom = None;
static unsigned long C_BG, C_FG, C_HEAD, C_FOOD, C_DIM;
static unsigned long C_GB_DARK, C_GB_MED, C_GB_LIGHT, C_GB_PALE;
@@ -120,6 +134,8 @@ static int score = 0, best = 0, lines = 0, level = 1;
static int speed = 600; // ms per gravity step (derived from pps below)
static int pps = 23; // base speed px/sec; +1 px/sec per 10 lines cleared
static bool running = true;
static bool dirty = true;
static bool resyncTimer = false;
static void piece_cells(int type, int rot, int out[4][2]) {
int r = ((rot % 4) + 4) % 4;
@@ -139,10 +155,14 @@ static bool collide(int px, int py, int cells[4][2]) {
return false;
}
static unsigned long now_ms() {
static int64_t now_ns() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
return (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec;
}
static inline int64_t step_ns() {
return (int64_t)speed * 1000000LL;
}
// Derive the gravity delay (ms) from the target pixels/second. A piece drops
@@ -166,6 +186,8 @@ static void spawn() {
state = GAMEOVER;
if (score > best) best = score;
}
resyncTimer = true;
dirty = true;
}
static void lock_piece() {
@@ -174,7 +196,12 @@ static void lock_piece() {
for (int i = 0; i < 4; i++) {
int cx = curX + cells[i][0];
int cy = curY + cells[i][1];
if (cy < 0) { state = GAMEOVER; if (score > best) best = score; return; }
if (cy < 0) {
state = GAMEOVER;
if (score > best) best = score;
dirty = true;
return;
}
board[cx][cy] = curType + 1;
}
// clear full lines
@@ -206,6 +233,7 @@ static void step() {
piece_cells(curType, curRot, cells);
if (!collide(curX, curY + 1, cells)) {
curY++;
dirty = true;
} else {
lock_piece();
}
@@ -223,7 +251,10 @@ static void hard_drop() {
static void try_move(int dx) {
int cells[4][2];
piece_cells(curType, curRot, cells);
if (!collide(curX + dx, curY, cells)) curX += dx;
if (!collide(curX + dx, curY, cells)) {
curX += dx;
dirty = true;
}
}
static void try_rotate() {
@@ -236,6 +267,7 @@ static void try_rotate() {
if (!collide(curX + kicks[k], curY, cells)) {
curX += kicks[k];
curRot = nr;
dirty = true;
return;
}
}
@@ -245,36 +277,81 @@ static void reset_game() {
for (int x = 0; x < PCOLS; x++) for (int y = 0; y < PROWS; y++) board[x][y] = 0;
score = 0; lines = 0; level = 1; update_speed();
nextType = rand() % 7;
spawn();
state = PLAYING;
spawn();
}
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
// Append a string's coalesced glyph rectangles into a caller-supplied array.
static int append_text_rects(int x, int y, int scale, const char* s,
XRectangle* rects, int maxRects, int nrects) {
int cx = x;
XSetForeground(dpy, gc, col);
for (const char* p = s; *p; p++) {
const unsigned char* g = glyph(*p);
for (int r = 0; r < 7; r++) {
unsigned char row = g[r];
for (int b = 0; b < 5; b++)
if (row & (1 << (4 - b)))
XFillRectangle(dpy, back, gc, cx + b * scale, y + r * scale, scale, scale);
if (!row) continue;
int b = 0;
while (b < 5) {
if (row & (1 << (4 - b))) {
int start = b;
while (b < 5 && (row & (1 << (4 - b)))) b++;
if (nrects < maxRects) {
rects[nrects].x = (short)(cx + start * scale);
rects[nrects].y = (short)(y + r * scale);
rects[nrects].width = (unsigned short)((b - start) * scale);
rects[nrects].height = (unsigned short)scale;
nrects++;
}
} else {
b++;
}
}
}
cx += 6 * scale;
}
return nrects;
}
// Draw a Gameboy-style beveled cell: medium body, pale top/left highlight,
// dark bottom/right shadow. bodycol is the fill shade (settled vs active).
static void draw_gb_cell(int x, int y, int size, unsigned long bodycol) {
XSetForeground(dpy, gc, bodycol);
XFillRectangle(dpy, back, gc, x + 1, y + 1, size - 2, size - 2);
XSetForeground(dpy, gc, C_GB_PALE);
XFillRectangle(dpy, back, gc, x + 1, y, size - 2, 1);
XFillRectangle(dpy, back, gc, x, y + 1, 1, size - 2);
XSetForeground(dpy, gc, C_GB_DARK);
XFillRectangle(dpy, back, gc, x + 1, y + size - 1, size - 2, 1);
XFillRectangle(dpy, back, gc, x + size - 1, y + 1, 1, size - 2);
static void draw_text(int x, int y, int scale, unsigned long col, const char* s) {
XRectangle rects[512];
int nrects = append_text_rects(x, y, scale, s, rects, 512, 0);
if (nrects > 0) {
XSetForeground(dpy, gc, col);
XFillRectangles(dpy, back, gc, rects, nrects);
}
}
static inline void append_bevel(int bx, int by, int size,
XRectangle* bodies, int& nb,
XRectangle* highlights, int& nh,
XRectangle* shadows, int& ns) {
bodies[nb].x = (short)(bx + 1);
bodies[nb].y = (short)(by + 1);
bodies[nb].width = (unsigned short)(size - 2);
bodies[nb].height = (unsigned short)(size - 2);
nb++;
highlights[nh].x = (short)(bx + 1);
highlights[nh].y = (short)by;
highlights[nh].width = (unsigned short)(size - 2);
highlights[nh].height = 1;
nh++;
highlights[nh].x = (short)bx;
highlights[nh].y = (short)(by + 1);
highlights[nh].width = 1;
highlights[nh].height = (unsigned short)(size - 2);
nh++;
shadows[ns].x = (short)(bx + 1);
shadows[ns].y = (short)(by + size - 1);
shadows[ns].width = (unsigned short)(size - 2);
shadows[ns].height = 1;
ns++;
shadows[ns].x = (short)(bx + size - 1);
shadows[ns].y = (short)(by + 1);
shadows[ns].width = 1;
shadows[ns].height = (unsigned short)(size - 2);
ns++;
}
static void render() {
@@ -282,113 +359,136 @@ static void render() {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
// single border framing the whole game (playfield + side panel) at the
// window edges, so it is visible on all four sides including right/bottom
// Outer border framing the whole game window
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, 2, 2, winW - 4, winH - 4);
// settled blocks (batched for speed)
XRectangle bodies[PCOLS * PROWS], highlights[PCOLS * PROWS * 2], shadows[PCOLS * PROWS * 2];
int nb = 0, nh = 0, ns = 0;
for (int x = 0; x < PCOLS; x++)
for (int y = 0; y < PROWS; y++)
if (board[x][y]) {
int bx = px0 + x * CELL, by = py0 + y * CELL;
bodies[nb].x = bx + 1; bodies[nb].y = by + 1;
bodies[nb].width = CELL - 2; bodies[nb].height = CELL - 2;
nb++;
highlights[nh].x = bx + 1; highlights[nh].y = by;
highlights[nh].width = CELL - 2; highlights[nh].height = 1;
nh++;
highlights[nh].x = bx; highlights[nh].y = by + 1;
highlights[nh].width = 1; highlights[nh].height = CELL - 2;
nh++;
shadows[ns].x = bx + 1; shadows[ns].y = by + CELL - 1;
shadows[ns].width = CELL - 2; shadows[ns].height = 1;
ns++;
shadows[ns].x = bx + CELL - 1; shadows[ns].y = by + 1;
shadows[ns].width = 1; shadows[ns].height = CELL - 2;
ns++;
}
XSetForeground(dpy, gc, C_GB_MED); XFillRectangles(dpy, back, gc, bodies, nb);
XSetForeground(dpy, gc, C_GB_PALE); XFillRectangles(dpy, back, gc, highlights, nh);
XSetForeground(dpy, gc, C_GB_DARK); XFillRectangles(dpy, back, gc, shadows, ns);
// active piece
if (state == PLAYING) {
// 1px frame around the 10x20 well so its walls/floor are unmistakable
XDrawRectangle(dpy, back, gc, px0 - 1, py0 - 1, PCOLS * CELL + 1, PROWS * CELL + 1);
// Side panel preview frame
int sx = px0 + PCOLS * CELL + 12;
int boxY = py0 + 10;
int boxH = 52;
XDrawRectangle(dpy, back, gc, sx - 2, boxY + 1, winW - sx - 3, boxH);
// Batched beveled blocks: settled board, active piece, and next-piece preview
XRectangle settledBodies[PCOLS * PROWS];
XRectangle activeBodies[4];
XRectangle previewBodies[4];
XRectangle highlights[(PCOLS * PROWS + 8) * 2];
XRectangle shadows[(PCOLS * PROWS + 8) * 2];
int nsb = 0, nab = 0, npb = 0, nh = 0, ns = 0;
for (int x = 0; x < PCOLS; x++) {
for (int y = 0; y < PROWS; y++) {
if (board[x][y]) {
append_bevel(px0 + x * CELL, py0 + y * CELL, CELL,
settledBodies, nsb, highlights, nh, shadows, ns);
}
}
}
int cells[4][2];
piece_cells(curType, curRot, cells);
for (int i = 0; i < 4; i++) {
int cx = curX + cells[i][0];
int cy = curY + cells[i][1];
if (cy >= 0)
draw_gb_cell(px0 + cx * CELL, py0 + cy * CELL, CELL, C_GB_LIGHT);
if (cy >= 0) {
append_bevel(px0 + cx * CELL, py0 + cy * CELL, CELL,
activeBodies, nab, highlights, nh, shadows, ns);
}
}
}
// vertical separator line between the playfield and the side panel
int sepX = px0 + PCOLS * CELL + 6;
XSetForeground(dpy, gc, C_FG);
XDrawLine(dpy, back, gc, sepX, py0, sepX, py0 + PROWS * CELL);
int nc[4][2];
piece_cells(nextType, 0, nc);
int minx = 4, miny = 4, maxx = -1, maxy = -1;
for (int i = 0; i < 4; i++) {
if (nc[i][0] < minx) minx = nc[i][0];
if (nc[i][1] < miny) miny = nc[i][1];
if (nc[i][0] > maxx) maxx = nc[i][0];
if (nc[i][1] > maxy) maxy = nc[i][1];
}
int pcell = 11;
int pcols = maxx - minx + 1;
int prows = maxy - miny + 1;
int nx0 = sx + ((winW - sx - 4) - pcols * pcell) / 2;
int ny0 = boxY + (boxH - prows * pcell) / 2 + 12;
for (int i = 0; i < 4; i++) {
int cx = nx0 + (nc[i][0] - minx) * pcell;
int cy = ny0 + (nc[i][1] - miny) * pcell;
append_bevel(cx, cy, pcell, previewBodies, npb, highlights, nh, shadows, ns);
}
// side panel
int sx = px0 + PCOLS * CELL + 12;
draw_text(sx, py0, 1, C_DIM, "NEXT");
// framed preview box for the next piece
int boxY = py0 + 10;
int boxH = 52;
XSetForeground(dpy, gc, C_DIM);
XDrawRectangle(dpy, back, gc, sx - 2, boxY + 1, winW - sx - 3, boxH);
int nc[4][2];
piece_cells(nextType, 0, nc);
int minx = 4, miny = 4, maxx = -1, maxy = -1;
for (int i = 0; i < 4; i++) {
minx = nc[i][0] < minx ? nc[i][0] : minx;
miny = nc[i][1] < miny ? nc[i][1] : miny;
maxx = nc[i][0] > maxx ? nc[i][0] : maxx;
maxy = nc[i][1] > maxy ? nc[i][1] : maxy;
}
int pcell = 11; // larger next-piece preview
int pcols = maxx - minx + 1;
int prows = maxy - miny + 1;
int nx0 = sx + ((winW - sx - 4) - pcols * pcell) / 2;
int ny0 = boxY + (boxH - prows * pcell) / 2 + 12; // sit lower in the frame
for (int i = 0; i < 4; i++) {
int cx = nx0 + (nc[i][0] - minx) * pcell;
int cy = ny0 + (nc[i][1] - miny) * pcell;
draw_gb_cell(cx, cy, pcell, C_HEAD);
}
int ty = boxY + boxH + 6;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score); draw_text(sx, ty, 1, C_DIM, buf);
snprintf(buf, sizeof(buf), "LINES %d", lines); draw_text(sx, ty + 9, 1, C_DIM, buf);
snprintf(buf, sizeof(buf), "LEVEL %d", level); draw_text(sx, ty + 18, 1, C_DIM, buf);
if (best > 0) { snprintf(buf, sizeof(buf), "BEST %d", best); draw_text(sx, ty + 27, 1, C_DIM, buf); }
if (nsb > 0) {
XSetForeground(dpy, gc, C_GB_MED);
XFillRectangles(dpy, back, gc, settledBodies, nsb);
}
if (nab > 0) {
XSetForeground(dpy, gc, C_GB_LIGHT);
XFillRectangles(dpy, back, gc, activeBodies, nab);
}
if (npb > 0) {
XSetForeground(dpy, gc, C_HEAD);
XFillRectangles(dpy, back, gc, previewBodies, npb);
}
if (nh > 0) {
XSetForeground(dpy, gc, C_GB_PALE);
XFillRectangles(dpy, back, gc, highlights, nh);
}
if (ns > 0) {
XSetForeground(dpy, gc, C_GB_DARK);
XFillRectangles(dpy, back, gc, shadows, ns);
}
// concise controls in the free space below the stats
int ky = ty + 27 + 12;
draw_text(sx, ky, 1, C_DIM, "KEYS");
draw_text(sx, ky + 9, 1, C_DIM, "4/6 MOVE");
draw_text(sx, ky + 18, 1, C_DIM, "2 ROTATE");
draw_text(sx, ky + 27, 1, C_DIM, "8 SOFT");
draw_text(sx, ky + 36, 1, C_DIM, "0 DROP");
draw_text(sx, ky + 45, 1, C_DIM, "# PAUSE");
draw_text(sx, ky + 54, 1, C_DIM, "5 OK");
// Vertical separator line between playfield and side panel
int sepX = px0 + PCOLS * CELL + 6;
XSetForeground(dpy, gc, C_FG);
XDrawLine(dpy, back, gc, sepX, py0, sepX, py0 + PROWS * CELL);
if (state == PAUSED || state == GAMEOVER) {
XSetForeground(dpy, gc, C_BG);
XFillRectangle(dpy, back, gc, 0, 0, winW, winH);
}
if (state == PAUSED) {
// Batch ALL side-panel text (NEXT, stats, and controls) into a single XFillRectangles call!
XRectangle textRects[1024];
int ntr = 0;
ntr = append_text_rects(sx, py0, 1, "NEXT", textRects, 1024, ntr);
int ty = boxY + boxH + 6;
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
ntr = append_text_rects(sx, ty, 1, buf, textRects, 1024, ntr);
snprintf(buf, sizeof(buf), "LINES %d", lines);
ntr = append_text_rects(sx, ty + 9, 1, buf, textRects, 1024, ntr);
snprintf(buf, sizeof(buf), "LEVEL %d", level);
ntr = append_text_rects(sx, ty + 18, 1, buf, textRects, 1024, ntr);
if (best > 0) {
snprintf(buf, sizeof(buf), "BEST %d", best);
ntr = append_text_rects(sx, ty + 27, 1, buf, textRects, 1024, ntr);
}
int ky = ty + 27 + 12;
ntr = append_text_rects(sx, ky, 1, "KEYS", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 9, 1, "4/6 MOVE", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 18, 1, "2 ROTATE", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 27, 1, "8 SOFT", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 36, 1, "0 DROP", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 45, 1, "# PAUSE", textRects, 1024, ntr);
ntr = append_text_rects(sx, ky + 54, 1, "5 OK", textRects, 1024, ntr);
if (ntr > 0) {
XSetForeground(dpy, gc, C_DIM);
XFillRectangles(dpy, back, gc, textRects, ntr);
}
} else if (state == PAUSED) {
draw_text(winW / 2 - 5 * 6, winH / 2 - 7, 2, C_FG, "PAUSE");
draw_text(winW / 2 - 4 * 6, winH / 2 + 10, 1, C_DIM, "5 RESUME");
} else if (state == GAMEOVER) {
draw_text(winW / 2 - 9 * 6, winH / 2 - 14, 2, C_FG, "GAME OVER");
snprintf(buf, sizeof(buf), "SCORE %d", score); draw_text(winW / 2 - 5 * 6, winH / 2 + 4, 1, C_DIM, buf);
char buf[32];
snprintf(buf, sizeof(buf), "SCORE %d", score);
draw_text(winW / 2 - (int)strlen(buf) * 3, winH / 2 + 4, 1, C_DIM, buf);
draw_text(winW / 2 - 5 * 6, winH / 2 + 13, 1, C_DIM, "5 NEW GAME");
}
XCopyArea(dpy, back, win, gc, 0, 0, winW, winH, 0, 0);
XFlush(dpy);
dirty = false;
}
static void setup_window() {
@@ -407,10 +507,15 @@ static void setup_window() {
if (winW < 96) winW = 96;
if (winH < 96) winH = 96;
win = XCreateWindow(dpy, RootWindow(dpy, screen), bw, TOPBAR + bw, winW, winH, 0,
CopyFromParent, InputOutput, CopyFromParent, 0, nullptr);
CopyFromParent, InputOutput, CopyFromParent, 0, nullptr);
XStoreName(dpy, win, "Tetris");
XClassHint* ch = XAllocClassHint();
if (ch) { ch->res_name = (char*)"tetris"; ch->res_class = (char*)"tetris"; XSetClassHint(dpy, win, ch); XFree(ch); }
if (ch) {
ch->res_name = (char*)"tetris";
ch->res_class = (char*)"tetris";
XSetClassHint(dpy, win, ch);
XFree(ch);
}
Atom wtype = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
Atom dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False);
@@ -422,10 +527,14 @@ static void setup_window() {
sh.min_height = sh.max_height = winH;
XSetWMNormalHints(dpy, win, &sh);
Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, win, &wm_del, 1);
wmDeleteAtom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, win, &wmDeleteAtom, 1);
gc = XCreateGC(dpy, win, 0, nullptr);
// CRITICAL: disable GraphicsExposures so XCopyArea does not emit a NoExpose
// event on every frame (which previously woke select() in an infinite 100%
// CPU redraw storm).
XSetGraphicsExposures(dpy, gc, False);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
C_BG = BlackPixel(dpy, screen);
@@ -439,121 +548,134 @@ static void setup_window() {
C_GB_LIGHT = 0x7fdf7f; // active piece body
C_GB_PALE = 0xbfffbf; // top/left highlight
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask);
XSelectInput(dpy, win, KeyPressMask | StructureNotifyMask | ExposureMask | FocusChangeMask);
XMapWindow(dpy, win);
}
static void handle_key(KeySym ks) {
switch (ks) {
case XK_Left: case XK_a: case XK_KP_Left: case XK_4:
if (state == PLAYING) { try_move(-1); }
case XK_Left: case XK_a: case XK_KP_Left: case XK_4: case XK_KP_4:
if (state == PLAYING) try_move(-1);
break;
case XK_Right: case XK_d: case XK_KP_Right: case XK_6:
if (state == PLAYING) { try_move(1); }
case XK_Right: case XK_d: case XK_KP_Right: case XK_6: case XK_KP_6:
if (state == PLAYING) try_move(1);
break;
case XK_Up: case XK_w: case XK_KP_Up: case XK_2:
if (state == PLAYING) { try_rotate(); }
case XK_Up: case XK_w: case XK_KP_Up: case XK_2: case XK_KP_2:
if (state == PLAYING) try_rotate();
break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x:
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_KP_8: case XK_x:
if (state == PLAYING) {
int cells[4][2];
piece_cells(curType, curRot, cells);
if (!collide(curX, curY + 1, cells)) {
curY++;
resyncTimer = true;
dirty = true;
} else {
lock_piece();
}
}
break;
case XK_space: case XK_0:
if (state == PLAYING) { hard_drop(); }
case XK_space: case XK_0: case XK_KP_0:
if (state == PLAYING) hard_drop();
else if (state == PAUSED) { state = PLAYING; resyncTimer = true; dirty = true; }
else if (state == GAMEOVER) reset_game();
break;
// Pause: P, 3, *, and the # key. NOTE: on this device the # key emits
// XK_KP_Divide (not XK_numbersign), so register KP_Divide too while the
// on-screen label still shows '#'.
case XK_p: case XK_3: case XK_numbersign: case XK_asterisk: case XK_KP_Divide:
// Pause: P, 3, *, and the # key (which emits XK_KP_Divide on nokia-argon).
case XK_p: case XK_3: case XK_KP_3:
case XK_numbersign: case XK_slash: case XK_KP_Divide:
case XK_asterisk: case XK_KP_Multiply:
if (state == PLAYING) {
state = PAUSED;
dirty = true;
} else if (state == PAUSED) {
state = PLAYING;
resyncTimer = true;
dirty = true;
}
break;
case XK_5: case XK_Return: case XK_KP_Enter:
case XK_5: case XK_KP_5: case XK_Return: case XK_KP_Enter:
if (state == PLAYING) {
hard_drop();
} else if (state == PAUSED) {
state = PLAYING;
resyncTimer = true;
dirty = true;
} else if (state == GAMEOVER) {
reset_game();
}
break;
case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End: case XK_Menu:
running = false; break;
case XK_Escape: case XK_BackSpace: case XK_Delete: case XK_End:
case XK_Menu: case XF86XK_Back: case XF86XK_KbdLcdMenu2:
running = false;
break;
default:
if (state == GAMEOVER) { reset_game(); }
break;
}
}
static void handle_event(XEvent& e) {
if (e.type == ClientMessage) {
Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
if ((Atom)e.xclient.data.l[0] == wm_del) running = false;
if ((Atom)e.xclient.data.l[0] == wmDeleteAtom) running = false;
} else if (e.type == ConfigureNotify) {
winW = e.xconfigure.width; winH = e.xconfigure.height;
if (e.xconfigure.width != winW || e.xconfigure.height != winH) {
winW = e.xconfigure.width;
winH = e.xconfigure.height;
if (back) XFreePixmap(dpy, back);
back = XCreatePixmap(dpy, win, winW, winH, DefaultDepth(dpy, screen));
dirty = true;
}
} else if (e.type == Expose) {
render();
if (e.xexpose.count == 0) dirty = true;
} else if (e.type == FocusOut) {
if (e.xfocus.mode == NotifyNormal && state == PLAYING) {
state = PAUSED;
dirty = true;
}
} else if (e.type == KeyPress) {
char buf[8];
KeySym ks = 0;
XLookupString(&e.xkey, buf, sizeof(buf), &ks, nullptr);
if (ks == NoSymbol) ks = XLookupKeysym(&e.xkey, 0);
if (ks == XK_q) { running = false; return; }
if (ks == XK_q || ks == XK_Q) { running = false; return; }
handle_key(ks);
}
}
int main() {
srand((unsigned)time(nullptr) ^ (unsigned)getpid());
init_glyph_lut();
setup_window();
reset_game();
render();
int xfd = ConnectionNumber(dpy);
// Precise, drift-free stepping via a kernel high-resolution timer (timerfd).
// The timer fires on an absolute CLOCK_MONOTONIC deadline, so wakeups are not
// quantized to the kernel tick the way a select() timeout is. A select()
// timeout shorter than one tick gets rounded UP to a whole tick, which made
// the final few milliseconds of each wait overshoot by up to ~20ms and showed
// up as speed jitter. Input still arrives on the X connection fd, so we
// select() on BOTH fds and react to either immediately -- there is no
// busy-spin, so the X server stays free and the controls stay responsive.
int tfd = -1;
#if defined(__linux__)
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
#endif
unsigned long nextStep = now_ms() + speed;
int64_t nextStep = now_ns() + step_ns();
State prevState = PLAYING;
resyncTimer = false;
auto arm_timer = [&](unsigned long deadline_ms) {
auto arm_timer = [&](int64_t deadline_ns) {
if (tfd < 0) return;
struct itimerspec its;
its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 0; // one-shot
its.it_value.tv_sec = (time_t)(deadline_ms / 1000);
its.it_value.tv_nsec = (long)((deadline_ms % 1000) * 1000000UL);
if (its.it_value.tv_nsec >= 1000000000L) {
its.it_value.tv_sec += 1; its.it_value.tv_nsec -= 1000000000L;
}
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 0;
its.it_value.tv_sec = (time_t)(deadline_ns / 1000000000LL);
its.it_value.tv_nsec = (long)(deadline_ns % 1000000000LL);
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, nullptr);
};
auto disarm_timer = [&]() {
if (tfd < 0) return;
struct itimerspec its;
memset(&its, 0, sizeof(its));
timerfd_settime(tfd, 0, &its, nullptr);
};
if (tfd >= 0) arm_timer(nextStep);
arm_timer(nextStep);
while (running) {
fd_set fds;
@@ -563,18 +685,15 @@ int main() {
struct timeval tvbuf, *tvp = nullptr;
if (tfd >= 0) {
// While playing, also wait on the timer fd. While paused / game over the
// timer is disarmed, so we block on input only (resume stays instant).
if (state == PLAYING) {
FD_SET(tfd, &fds);
if (tfd + 1 > nfds) nfds = tfd + 1;
}
} else {
// Fallback (timerfd unavailable): bounded select so input stays live.
long sleepMs = (state == PLAYING) ? (long)nextStep - (long)now_ms() : 30;
if (sleepMs < 0) sleepMs = 0;
tvbuf.tv_sec = sleepMs / 1000;
tvbuf.tv_usec = (sleepMs % 1000) * 1000;
} else if (state == PLAYING) {
int64_t remNs = nextStep - now_ns();
if (remNs < 0) remNs = 0;
tvbuf.tv_sec = (time_t)(remNs / 1000000000LL);
tvbuf.tv_usec = (suseconds_t)((remNs % 1000000000LL) / 1000LL);
tvp = &tvbuf;
}
@@ -592,42 +711,51 @@ int main() {
if (!running) break;
}
// 2) (Re)start play cleanly: resync the clock on resume / restart / launch,
// and stop the timer whenever we leave the PLAYING state.
if (state != prevState) {
if (state == PLAYING) { nextStep = now_ms() + speed; if (tfd >= 0) arm_timer(nextStep); }
else if (tfd >= 0) disarm_timer();
// 2) Resync timer on state transitions, soft drop, or piece spawn.
if (state != prevState || resyncTimer) {
if (state == PLAYING) {
nextStep = now_ns() + step_ns();
arm_timer(nextStep);
} else {
disarm_timer();
}
prevState = state;
}
// 3) Advance the simulation exactly once per timer expiration (or, in the
// fallback path, whenever the deadline is reached). If we ever fell more
// than one interval behind (e.g. the process was frozen for a screenshot)
// we drop the backlog instead of racing to catch up.
if (state == PLAYING) {
resyncTimer = false;
} else if (state == PLAYING) {
// 3) Advance simulation on scheduled gravity timer expiration.
bool due = false;
if (tfd >= 0) {
if (FD_ISSET(tfd, &fds)) {
uint64_t expirations = 0;
ssize_t n = read(tfd, &expirations, sizeof(expirations));
(void)n; (void)expirations;
(void)n;
due = true;
}
} else {
if ((long)(now_ms() - nextStep) >= 0) due = true;
if (now_ns() >= nextStep) due = true;
}
if (due) {
step();
nextStep += speed; // advance by exactly one interval (no drift)
unsigned long now = now_ms();
if ((long)(now - nextStep) > (long)speed) nextStep = now + speed;
if (tfd >= 0 && state == PLAYING) arm_timer(nextStep);
if (state == PLAYING) {
if (resyncTimer) {
nextStep = now_ns() + step_ns();
resyncTimer = false;
} else {
nextStep += step_ns();
int64_t now = now_ns();
if (now - nextStep > step_ns()) nextStep = now + step_ns();
}
arm_timer(nextStep);
} else {
disarm_timer();
prevState = state;
resyncTimer = false;
}
}
}
// 4) Repaint on every loop turn (input or step) so the display never
// appears frozen and key feedback is immediate.
render();
// 4) Repaint only when visual state actually changed.
if (dirty) render();
}
if (tfd >= 0) close(tfd);
+355 -24
View File
@@ -699,21 +699,46 @@ spawn_detached_sh(const char *cmd)
}
static void
trigger_plugin_open(const char *plugin_name)
trigger_plugin_open(const char *plugin_spec)
{
if (!plugin_spec || !plugin_spec[0])
return;
char plugin_name[64];
const char *arg = NULL;
const char *colon = strchr(plugin_spec, ':');
if (colon) {
size_t nlen = (size_t)(colon - plugin_spec);
if (nlen >= sizeof(plugin_name))
nlen = sizeof(plugin_name) - 1;
memcpy(plugin_name, plugin_spec, nlen);
plugin_name[nlen] = '\0';
arg = colon + 1;
} else {
snprintf(plugin_name, sizeof(plugin_name), "%s", plugin_spec);
}
char fifo_path[128];
snprintf(fifo_path, sizeof(fifo_path), "/tmp/dwm_plugin_%s.fifo", plugin_name);
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK | O_CLOEXEC);
if (fd >= 0) {
const char *msg = (strcmp(plugin_name, "xdrawer") == 0) ? "TOGGLE\n" : "OPEN\n";
char msg[128];
if (strcmp(plugin_name, "xdrawer") == 0)
snprintf(msg, sizeof(msg), "TOGGLE\n");
else if (arg && arg[0])
snprintf(msg, sizeof(msg), "OPEN %s\n", arg);
else
snprintf(msg, sizeof(msg), "OPEN\n");
if (write(fd, msg, strlen(msg)) > 0) {
close(fd);
return;
}
close(fd);
}
char cmd[128];
snprintf(cmd, sizeof(cmd), "dwm-plugin %s", plugin_name);
char cmd[160];
if (arg && arg[0])
snprintf(cmd, sizeof(cmd), "dwm-plugin %s %s", plugin_name, arg);
else
snprintf(cmd, sizeof(cmd), "dwm-plugin %s", plugin_name);
spawn_detached_sh(cmd);
}
@@ -841,9 +866,75 @@ handle_builtin_toggle(const char *toggle_name)
close(fd);
}
} else if (strcmp(toggle_name, "bluetooth") == 0) {
int rc = system("doas sxmo_bluetoothtoggle.sh >/dev/null 2>&1");
int rc = system("SXMO_OS=alpine doas -n sxmo_bluetoothtoggle.sh >/dev/null 2>&1");
(void)rc;
spawn_detached_sh("sxmo_hook_statusbar.sh bluetooth");
} else if (strcmp(toggle_name, "ring") == 0) {
const char *home = getenv("HOME");
if (!home || !home[0])
home = "/home/user";
char dir[256], path[256];
snprintf(dir, sizeof(dir), "%s/.config/sxmo", home);
snprintf(path, sizeof(path), "%s/.noring", dir);
if (access(path, F_OK) == 0) {
unlink(path);
} else {
mkdir(dir, 0755);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd >= 0)
close(fd);
}
spawn_detached_sh("sxmo_hook_statusbar.sh audio");
} else if (strcmp(toggle_name, "vibrate") == 0) {
const char *home = getenv("HOME");
if (!home || !home[0])
home = "/home/user";
char dir[256], path[256];
snprintf(dir, sizeof(dir), "%s/.config/sxmo", home);
snprintf(path, sizeof(path), "%s/.novibrate", dir);
if (access(path, F_OK) == 0) {
unlink(path);
} else {
mkdir(dir, 0755);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd >= 0)
close(fd);
}
spawn_detached_sh("sxmo_hook_statusbar.sh audio");
} else if (strcmp(toggle_name, "dnd") == 0) {
const char *rdir = getenv("XDG_RUNTIME_DIR");
char path[256];
snprintf(path, sizeof(path), "%s/sxmo.busy", (rdir && rdir[0]) ? rdir : "/run/user/10000");
if (access(path, F_OK) == 0) {
unlink(path);
} else {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd >= 0)
close(fd);
}
spawn_detached_sh("sxmo_hook_statusbar.sh audio");
} else if (strcmp(toggle_name, "notif_clear_all") == 0) {
const char *ndir = getenv("SXMO_NOTIFDIR");
char default_dir[256];
if (!ndir || !ndir[0]) {
const char *home = getenv("HOME");
snprintf(default_dir, sizeof(default_dir), "%s/.local/share/sxmo/notifications",
(home && home[0]) ? home : "/home/user");
ndir = default_dir;
}
DIR *d = opendir(ndir);
if (d) {
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
if (ent->d_name[0] == '.')
continue;
char fpath[512];
snprintf(fpath, sizeof(fpath), "%s/%s", ndir, ent->d_name);
unlink(fpath);
}
closedir(d);
}
spawn_detached_sh("sxmo_hook_statusbar.sh notifs");
}
}
@@ -926,6 +1017,27 @@ activate_selection(unsigned int ev_state)
trigger_plugin_open(act + 8);
return;
}
if (strncmp(act, "@notif_open:", 12) == 0) {
const char *fpath = act + 12;
char action_cmd[512] = "";
FILE *fp = fopen(fpath, "r");
if (fp) {
char line1[256];
if (fgets(line1, sizeof(line1), fp) && fgets(action_cmd, sizeof(action_cmd), fp)) {
char *nl = strpbrk(action_cmd, "\r\n");
if (nl)
*nl = '\0';
}
fclose(fp);
}
unlink(fpath);
spawn_detached_sh("sxmo_hook_statusbar.sh notifs");
hide_menu();
menu_mode = MODE_NONE;
if (action_cmd[0] && strcmp(action_cmd, "none") != 0)
spawn_detached_sh(action_cmd);
return;
}
if (strncmp(act, "@toggle:", 8) == 0) {
handle_builtin_toggle(act + 8);
char cur_menu[64];
@@ -1628,6 +1740,135 @@ load_cache_or_hook(const char *cache_name, const char *hook_cmd)
fclose(fp);
}
static void
populate_notifications_menu(void)
{
const char *ndir = getenv("SXMO_NOTIFDIR");
char default_dir[256];
if (!ndir || !ndir[0]) {
const char *home = getenv("HOME");
snprintf(default_dir, sizeof(default_dir), "%s/.local/share/sxmo/notifications",
(home && home[0]) ? home : "/home/user");
ndir = default_dir;
}
DIR *d = opendir(ndir);
if (!d)
return;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
if (ent->d_name[0] == '.')
continue;
char fpath[512];
snprintf(fpath, sizeof(fpath), "%s/%s", ndir, ent->d_name);
struct stat st;
if (stat(fpath, &st) != 0 || !S_ISREG(st.st_mode))
continue;
FILE *fp = fopen(fpath, "r");
if (!fp)
continue;
char l1[256] = "", l2[256] = "", l3[512] = "";
(void)fgets(l1, sizeof(l1), fp);
(void)fgets(l2, sizeof(l2), fp);
if (fgets(l3, sizeof(l3), fp)) {
char *nl = strpbrk(l3, "\r\n");
if (nl)
*nl = '\0';
}
fclose(fp);
if (!l3[0])
snprintf(l3, sizeof(l3), "%s", ent->d_name);
char label[560], act[560];
snprintf(label, sizeof(label), "%s %s", ICON_BEL, l3);
snprintf(act, sizeof(act), "@notif_open:%s", fpath);
add_menu_item(label, act, 0);
}
closedir(d);
}
static int
cmp_cstr_ptr(const void *a, const void *b)
{
const char *sa = *(const char *const *)a;
const char *sb = *(const char *const *)b;
return strcmp(sa, sb);
}
static void
scan_zoneinfo_recursive(const char *base_dir, const char *rel_dir,
char ***list_io, size_t *count_io, size_t *cap_io)
{
char full_dir[512];
if (rel_dir && rel_dir[0])
snprintf(full_dir, sizeof(full_dir), "%s/%s", base_dir, rel_dir);
else
snprintf(full_dir, sizeof(full_dir), "%s", base_dir);
DIR *d = opendir(full_dir);
if (!d)
return;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
const char *nm = ent->d_name;
if (nm[0] == '.' || strcmp(nm, "posix") == 0 || strcmp(nm, "right") == 0 ||
strcmp(nm, "zone.tab") == 0 || strcmp(nm, "zone1970.tab") == 0 ||
strcmp(nm, "iso3166.tab") == 0 || strcmp(nm, "leapseconds") == 0 ||
strcmp(nm, "tzdata.zi") == 0 || strcmp(nm, "localtime") == 0 ||
strcmp(nm, "posixrules") == 0)
continue;
char rel_path[512], child_full[512];
if (rel_dir && rel_dir[0])
snprintf(rel_path, sizeof(rel_path), "%s/%s", rel_dir, nm);
else
snprintf(rel_path, sizeof(rel_path), "%s", nm);
snprintf(child_full, sizeof(child_full), "%s/%s", base_dir, rel_path);
struct stat st;
if (stat(child_full, &st) != 0)
continue;
if (S_ISDIR(st.st_mode)) {
scan_zoneinfo_recursive(base_dir, rel_path, list_io, count_io, cap_io);
} else if (S_ISREG(st.st_mode)) {
if (*count_io >= *cap_io) {
size_t ncap = (*cap_io == 0) ? 256 : (*cap_io * 2);
char **nlist = realloc(*list_io, ncap * sizeof(char *));
if (!nlist)
break;
*list_io = nlist;
*cap_io = ncap;
}
(*list_io)[*count_io] = strdup(rel_path);
if ((*list_io)[*count_io])
(*count_io)++;
}
}
closedir(d);
}
static void
populate_timezone_menu(void)
{
char **tz_list = NULL;
size_t tz_count = 0, tz_cap = 0;
scan_zoneinfo_recursive("/usr/share/zoneinfo", "", &tz_list, &tz_count, &tz_cap);
if (tz_count > 1)
qsort(tz_list, tz_count, sizeof(char *), cmp_cstr_ptr);
for (size_t i = 0; i < tz_count; i++) {
char cmd[512];
snprintf(cmd, sizeof(cmd),
"doas -n setup-timezone -z '%s' >/dev/null 2>&1; sxmo_hook_statusbar.sh time",
tz_list[i]);
add_menu_item(tz_list[i], cmd, 0);
free(tz_list[i]);
}
free(tz_list);
}
static void
open_builtin_menu(const char *menu_name, int keep_idx)
{
@@ -1667,7 +1908,7 @@ open_builtin_menu(const char *menu_name, int keep_idx)
int nnotifs = count_notifications();
if (nnotifs > 0) {
snprintf(label, sizeof(label), "%s Notifications (%d)", ICON_BEL, nnotifs);
add_menu_item(label, "sxmo_notifs.sh menu", 0);
add_menu_item(label, "@menu:notifications", 1);
}
const char *sh = getenv("SHELL");
if (!sh || !sh[0])
@@ -1702,7 +1943,7 @@ open_builtin_menu(const char *menu_name, int keep_idx)
if (bt_is_unblocked_fast()) {
snprintf(label, sizeof(label), "%s Bluetooth", ICON_BTH);
add_menu_item(label, "sxmo_bluetoothmenu.sh", 0);
add_menu_item(label, "@plugin:networks:bluetooth", 0);
}
snprintf(label, sizeof(label), "%s Camera", ICON_CAM);
@@ -1720,16 +1961,16 @@ open_builtin_menu(const char *menu_name, int keep_idx)
}
snprintf(label, sizeof(label), "%s Networks", ICON_NET);
add_menu_item(label, "sxmo_networks.sh", 0);
add_menu_item(label, "@plugin:networks", 0);
snprintf(label, sizeof(label), "%s Audio", ICON_MUS);
add_menu_item(label, "sxmo_audio.sh", 0);
add_menu_item(label, "@menu:audio", 1);
snprintf(label, sizeof(label), "%s Modem", ICON_PHN);
add_menu_item(label, "@menu:modem", 1);
add_menu_item(label, "@plugin:modem", 0);
snprintf(label, sizeof(label), "%s Windows", ICON_WIN);
add_menu_item(label, "sxmo_wmmenu.sh", 0);
add_menu_item(label, "@menu:wm", 1);
snprintf(label, sizeof(label), "%s Config", ICON_CFG);
add_menu_item(label, "@menu:config", 1);
@@ -1767,12 +2008,26 @@ open_builtin_menu(const char *menu_name, int keep_idx)
} else if (strcmp(menu_name, "games") == 0) {
reset_session_defaults(1, "Games");
append_active_call_entries();
load_cache_or_hook("sxmo_games.cache", "sxmo_hook_games.sh");
if (access("/usr/bin/snake", X_OK) == 0) {
snprintf(label, sizeof(label), "%s Snake", ICON_GAM);
add_menu_item(label, "snake", 0);
}
if (access("/usr/bin/tetris", X_OK) == 0) {
snprintf(label, sizeof(label), "%s Tetris", ICON_GAM);
add_menu_item(label, "tetris", 0);
}
const char *rdir = getenv("XDG_RUNTIME_DIR");
char gcache[256];
snprintf(gcache, sizeof(gcache), "%s/sxmo_games.cache", (rdir && rdir[0]) ? rdir : "/run/user/10000");
if (access(gcache, R_OK) == 0 || access("/tmp/sxmo_games.cache", R_OK) == 0)
load_cache_or_hook("sxmo_games.cache", "sxmo_hook_games.sh");
else
spawn_detached_sh("sxmo_hook_games.sh >/dev/null 2>&1");
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
add_menu_item(label, "@menu:sys", 1);
snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS);
add_menu_item(label, "@close", 0);
} else if (strcmp(menu_name, "modem") == 0) {
} else if (strcmp(menu_name, "scripts") == 0) {
reset_session_defaults(1, "Scripts");
append_active_call_entries();
load_cache_or_hook("sxmo_scripts.cache", "sxmo_hook_scripts.sh");
@@ -1783,16 +2038,87 @@ open_builtin_menu(const char *menu_name, int keep_idx)
} else if (strcmp(menu_name, "modem") == 0) {
reset_session_defaults(1, "Modem");
append_active_call_entries();
snprintf(label, sizeof(label), "%s Modem PIN", ICON_PLK);
add_menu_item(label, "sxmo_unlocksim.sh", 0);
snprintf(label, sizeof(label), "%s Modem Info", ICON_INF);
add_menu_item(label, "sxmo_modeminfo.sh", 0);
snprintf(label, sizeof(label), "%s Modem Log", ICON_PHL);
add_menu_item(label, "sxmo_modemlog.sh", 0);
snprintf(label, sizeof(label), "%s SIM & Dual-SIM Manager", ICON_PHN);
add_menu_item(label, "@plugin:modem", 0);
snprintf(label, sizeof(label), "%s Unlock SIM (PIN / PUK)", ICON_PLK);
add_menu_item(label, "@plugin:modem:unlock", 0);
snprintf(label, sizeof(label), "%s Modem & Radio Status", ICON_INF);
add_menu_item(label, "@plugin:modem:info", 0);
snprintf(label, sizeof(label), "%s Send USSD Code", ICON_PHN);
add_menu_item(label, "@plugin:modem:ussd", 0);
snprintf(label, sizeof(label), "%s Modem Event Log", ICON_PHL);
add_menu_item(label, "@plugin:modem:log", 0);
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
add_menu_item(label, "@menu:sys", 1);
snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS);
add_menu_item(label, "@close", 0);
} else if (strcmp(menu_name, "audio") == 0) {
reset_session_defaults(1, "Audio");
append_active_call_entries();
const char *home = getenv("HOME");
if (!home || !home[0])
home = "/home/user";
const char *rdir = getenv("XDG_RUNTIME_DIR");
if (!rdir || !rdir[0])
rdir = "/run/user/10000";
char pbuf[256];
snprintf(label, sizeof(label), "%s Volume & Controls (XDrawer)", ICON_MUS);
add_menu_item(label, "@plugin:xdrawer", 0);
snprintf(pbuf, sizeof(pbuf), "%s/.config/sxmo/.noring", home);
snprintf(label, sizeof(label), "%s Ring %s", ICON_BEL, (access(pbuf, F_OK) == 0) ? ICON_TOF : ICON_TON);
add_menu_item(label, "@toggle:ring", 1);
snprintf(pbuf, sizeof(pbuf), "%s/.config/sxmo/.novibrate", home);
snprintf(label, sizeof(label), "%s Vibrate %s", ICON_BEL, (access(pbuf, F_OK) == 0) ? ICON_TOF : ICON_TON);
add_menu_item(label, "@toggle:vibrate", 1);
snprintf(pbuf, sizeof(pbuf), "%s/sxmo.busy", rdir);
snprintf(label, sizeof(label), "%s Do Not Disturb %s", ICON_ZZZ, (access(pbuf, F_OK) == 0) ? ICON_TON : ICON_TOF);
add_menu_item(label, "@toggle:dnd", 1);
snprintf(label, sizeof(label), "%s Route to Speaker", ICON_MUS);
add_menu_item(label, "amixer -c 0 cset name='SPK DAC Switch' on >/dev/null 2>&1; sxmo_hook_statusbar.sh audio", 1);
snprintf(label, sizeof(label), "%s Route to Earpiece", ICON_PHN);
add_menu_item(label, "amixer -c 0 cset name='SPK DAC Switch' off >/dev/null 2>&1; sxmo_hook_statusbar.sh audio", 1);
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
add_menu_item(label, "@menu:sys", 1);
snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS);
add_menu_item(label, "@close", 0);
} else if (strcmp(menu_name, "notifications") == 0) {
reset_session_defaults(1, "Notifs");
populate_notifications_menu();
snprintf(label, sizeof(label), "%s Clear All Notifications", ICON_CLS);
add_menu_item(label, "@toggle:notif_clear_all", 1);
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
add_menu_item(label, "@menu:sys", 1);
snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS);
add_menu_item(label, "@close", 0);
} else if (strcmp(menu_name, "wm") == 0) {
reset_session_defaults(1, "WM Menu");
for (int ws = 1; ws <= 4; ws++) {
snprintf(label, sizeof(label), "%d Move to WS %d", ws, ws);
snprintf(cmd, sizeof(cmd), "sxmo_wm.sh moveworkspace %d", ws);
add_menu_item(label, cmd, 1);
}
snprintf(label, sizeof(label), "%s Shift stack", ICON_RLD);
add_menu_item(label, "sxmo_wm.sh switchfocus", 1);
snprintf(label, sizeof(label), "%s Toggle Layout", ICON_GRD);
add_menu_item(label, "sxmo_wm.sh togglelayout", 1);
snprintf(label, sizeof(label), "%s Kill window", ICON_CLS);
add_menu_item(label, "sxmo_killwindow.sh", 0);
snprintf(label, sizeof(label), "%s System Menu", ICON_MNU);
add_menu_item(label, "@menu:sys", 1);
snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS);
add_menu_item(label, "@close", 0);
} else if (strcmp(menu_name, "timezone") == 0) {
reset_session_defaults(1, "Timezone");
snprintf(label, sizeof(label), "%s Back to Config", ICON_CFG);
add_menu_item(label, "@menu:config", 1);
populate_timezone_menu();
} else if (strcmp(menu_name, "config") == 0) {
reset_session_defaults(1, "Config");
append_active_call_entries();
@@ -1814,7 +2140,7 @@ open_builtin_menu(const char *menu_name, int keep_idx)
add_menu_item(label, "@toggle:bluetooth", 1);
snprintf(label, sizeof(label), "%s Change Timezone", ICON_CLK);
add_menu_item(label, "sxmo_timezonechange.sh", 0);
add_menu_item(label, "@menu:timezone", 1);
snprintf(pbuf, sizeof(pbuf), "%s/.cache/sxmo/sxmo.nosuspend", home);
snprintf(label, sizeof(label), "%s Auto-suspend %s", ICON_ZZZ, (access(pbuf, F_OK) == 0) ? ICON_TOF : ICON_TON);
@@ -1967,6 +2293,8 @@ close_other_active_plugins(void)
{ "xfiles.pid", "/tmp/dwm_plugin_files.fifo" },
{ "xcamera.pid", "/tmp/dwm_plugin_camera.fifo" },
{ "xthemes.pid", "/tmp/dwm_plugin_themes.fifo" },
{ "xmodem.pid", "/tmp/dwm_plugin_modem.fifo" },
{ "xnetworks.pid", "/tmp/dwm_plugin_networks.fifo" },
};
const char *rdir = getenv("XDG_RUNTIME_DIR");
const char *base = (rdir && rdir[0]) ? rdir : "/run/user/10000";
@@ -2855,10 +3183,13 @@ dmenu_run_main(int argc, char *argv[])
if (!strcmp(argv[i], "--preload")) {
preload = 1;
} else if (!strcmp(argv[i], "sys") || !strcmp(argv[i], "ctx") ||
!strcmp(argv[i], "applications") || !strcmp(argv[i], "scripts") ||
!strcmp(argv[i], "modem") || !strcmp(argv[i], "config") ||
!strcmp(argv[i], "power") || !strcmp(argv[i], "st") ||
!strcmp(argv[i], "nano") || !strcmp(argv[i], "sthotkeys")) {
!strcmp(argv[i], "applications") || !strcmp(argv[i], "games") ||
!strcmp(argv[i], "scripts") || !strcmp(argv[i], "modem") ||
!strcmp(argv[i], "audio") || !strcmp(argv[i], "notifications") ||
!strcmp(argv[i], "wm") || !strcmp(argv[i], "timezone") ||
!strcmp(argv[i], "config") || !strcmp(argv[i], "power") ||
!strcmp(argv[i], "st") || !strcmp(argv[i], "nano") ||
!strcmp(argv[i], "sthotkeys")) {
preload = 1;
initial_builtin = argv[i];
} else if (!strcmp(argv[i], "-v")) {
+1 -1
View File
@@ -1583,7 +1583,7 @@ is_right_softkey(KeySym ks, unsigned int keycode)
#include <dlfcn.h>
#include "dwm_plugin.h"
#define MAX_DWM_PLUGINS 16
#define MAX_DWM_PLUGINS 24
typedef struct {
void *handle;
+76 -7
View File
@@ -42,7 +42,7 @@ constexpr int kWinX = 0;
constexpr int kWinY = kTopBarHeight;
constexpr int kWinWidth = kScreenWidth;
constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar
constexpr int kRowCount = 7;
constexpr int kRowCount = 8;
// Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font")
constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker
@@ -54,6 +54,8 @@ constexpr const char* kIconMicOn = "\xf3\xb0\x8d\xac"; // U+F036C mic on
constexpr const char* kIconMicOff = "\xf3\xb0\x8d\xad"; // U+F036D mic off
constexpr const char* kIconBrightness = "\xf3\xb0\x83\x9d"; // U+F00DD brightness
constexpr const char* kIconKeyboard = "\xef\x84\x9c"; // U+F11C keyboard
constexpr const char* kIconBell = "\xef\x83\xb3"; // U+F0F3 bell / ringer
constexpr const char* kIconBellSlash = "\xef\x87\xb6"; // U+F1F6 bell-slash
constexpr const char* kIconToggleOn = "\xef\x88\x85"; // U+F205 toggle on
constexpr const char* kIconToggleOff = "\xef\x88\x84"; // U+F204 toggle off
@@ -240,8 +242,9 @@ enum class ControlId {
Flashlight = 2,
SoundMute = 3,
MicMute = 4,
ScreenBright = 5,
KbdBright = 6,
RingerMode = 5,
ScreenBright = 6,
KbdBright = 7,
};
struct ControlItem {
@@ -314,7 +317,12 @@ class PhoneController {
items_.push_back({ControlId::MicMute, kIconMicOn, kIconMicOff,
"Microphone", false, 0, 1, 1, (dec1 > 0) ? 1 : 0});
// 6. Screen brightness (slider 1..screenMax_)
// 6. Ringer Mode (0=Ring+Vib, 1=Vibrate, 2=Silent, 3=DND)
int ringerVal = readRingerMode();
items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash,
"Ringer", false, 0, 3, 1, ringerVal});
// 7. Screen brightness (slider 1..screenMax_)
int scrCur = std::clamp(readIntFile(backlightDir_ + "/brightness", screenMax_), 1, screenMax_);
int scrStep = std::max(1, screenMax_ / 14);
items_.push_back({ControlId::ScreenBright, kIconBrightness, kIconBrightness,
@@ -342,6 +350,8 @@ class PhoneController {
ControlItem& it = items_[idx];
if (it.isSlider) {
it.value = std::clamp(it.value + dir * it.stepVal, it.minVal, it.maxVal);
} else if (it.id == ControlId::RingerMode) {
it.value = (it.value + dir + 4) % 4;
} else {
it.value = it.value ? 0 : 1;
}
@@ -353,13 +363,38 @@ class PhoneController {
return;
}
ControlItem& it = items_[idx];
if (!it.isSlider) {
if (it.id == ControlId::RingerMode) {
it.value = (it.value + 1) % 4;
apply(it);
} else if (!it.isSlider) {
it.value = it.value ? 0 : 1;
apply(it);
}
}
private:
static std::string getSxmoCfgDir() {
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg && xdg[0]) {
return std::string(xdg) + "/sxmo";
}
const char* home = std::getenv("HOME");
return std::string((home && home[0]) ? home : "/home/user") + "/.config/sxmo";
}
static int readRingerMode() {
std::string busyFile = getRuntimeFile("sxmo.busy");
if (::access(busyFile.c_str(), F_OK) == 0) {
return 3; // DND
}
std::string cfg = getSxmoCfgDir();
bool noRing = (::access((cfg + "/.noring").c_str(), F_OK) == 0);
bool noVib = (::access((cfg + "/.novibrate").c_str(), F_OK) == 0);
if (noRing && noVib) return 2; // Silent
if (noRing) return 1; // Vibrate only
return 0; // Ring + Vibrate
}
void apply(ControlItem& it) {
switch (it.id) {
case ControlId::SpeakerVol: {
@@ -404,6 +439,30 @@ class PhoneController {
: "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1 >/dev/null 2>&1");
break;
}
case ControlId::RingerMode: {
std::string cfg = getSxmoCfgDir();
::mkdir(cfg.c_str(), 0755);
std::string noRingPath = cfg + "/.noring";
std::string noVibPath = cfg + "/.novibrate";
std::string busyPath = getRuntimeFile("sxmo.busy");
if (it.value == 0) { // Ring + Vibrate
::unlink(noRingPath.c_str());
::unlink(noVibPath.c_str());
::unlink(busyPath.c_str());
} else if (it.value == 1) { // Vibrate only
writeIntFile(noRingPath, 1);
::unlink(noVibPath.c_str());
::unlink(busyPath.c_str());
} else if (it.value == 2) { // Silent
writeIntFile(noRingPath, 1);
writeIntFile(noVibPath, 1);
::unlink(busyPath.c_str());
} else { // DND
writeIntFile(busyPath, 1);
}
runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1");
break;
}
case ControlId::ScreenBright: {
writeIntFile(backlightDir_ + "/bl_power", 0);
writeIntFile(backlightDir_ + "/brightness", it.value);
@@ -597,10 +656,11 @@ static int xdrawer_run_main(int argc, char** argv) {
}
const ControlItem& it = items[i];
bool activeState = (it.value > 0);
bool activeState = (it.id == ControlId::RingerMode) ? (it.value == 0 || it.value == 1)
: (it.value > 0);
const char* rowIcon = activeState ? it.iconOn : it.iconOff;
int labelMaxW = it.isSlider ? 82 : 136;
int labelMaxW = it.isSlider ? 82 : 126;
drawPango(layoutIcon, isSel ? &colYellow : &colLight, 8, ry, rowH, rowIcon);
drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry, rowH,
it.label, labelMaxW);
@@ -643,6 +703,13 @@ static int xdrawer_run_main(int argc, char** argv) {
int pw = pangoWidth(layoutSmall, pctStr);
drawPango(layoutSmall, isSel ? &colWhite : &colLight, kWinWidth - pw - 6,
ry, rowH, pctStr);
} else if (it.id == ControlId::RingerMode) {
const char* rModes[4] = {"Ring+Vib", "Vibrate", "Silent", "DND"};
std::string stStr = std::string("< ") + rModes[std::clamp(it.value, 0, 3)] + " >";
int stW = pangoWidth(layoutSmall, stStr);
XftColor* rCol = (it.value == 0) ? (isSel ? &colYellow : &colGreen)
: ((it.value == 1) ? &colYellow : &colMuted);
drawPango(layoutSmall, rCol, kWinWidth - stW - 6, ry, rowH, stStr);
} else {
bool on = (it.value != 0);
std::string stStr = on ? "ON" : "OFF";
@@ -715,6 +782,8 @@ static int xdrawer_run_main(int argc, char** argv) {
closeOtherPlugin("xfiles.pid", "/tmp/dwm_plugin_files.fifo");
closeOtherPlugin("xcamera.pid", "/tmp/dwm_plugin_camera.fifo");
closeOtherPlugin("xthemes.pid", "/tmp/dwm_plugin_themes.fifo");
closeOtherPlugin("xmodem.pid", "/tmp/dwm_plugin_modem.fifo");
closeOtherPlugin("xnetworks.pid", "/tmp/dwm_plugin_networks.fifo");
theme.checkAndReload(dpy, screen, visual, cmap, layoutMain, layoutSmall,
layoutIcon, nullptr, win);
openStartMs = nowMs();