Add modem/networks DWM plugins, Dual-SIM & PIN/PUK support, built-in dmenu views, and game UX fixes
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user