Add modem/networks DWM plugins, Dual-SIM & PIN/PUK support, built-in dmenu views, and game UX fixes
This commit is contained in:
@@ -7,23 +7,26 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f
|
||||
|
||||
## 2. Repository Layout
|
||||
- `src/sxmo-dwm/`: Patched `dwm` window manager (`240x320` vertical `bstack` layout, 24px Pango status bar, desktop `"Menu"`/`"Names"` softkey bar, C-level `SW_LID` slider and locked-key wake handling, live `.Xresources` reload, and `.so` plugin loader via `dwm_plugin.h`).
|
||||
- `src/sxmo-dmenu/`: Zero-lag resident DWM menu plugin (`dmenu.so`) and `/tmp/dwm_plugin_dmenu.sock` client (`dmenu` / `xdmenu`), featuring built-in Sxmo menus (`sys`, `applications`, `scripts`, `modem`, `config`, `power`, `st`, `nano`, `sthotkeys`) and `fcitx5-t9` XIM input.
|
||||
- `src/sxmo-dmenu/`: Zero-lag resident DWM menu plugin (`dmenu.so`) and `/tmp/dwm_plugin_dmenu.sock` client (`dmenu` / `xdmenu`), featuring built-in Sxmo menus (`sys`, `applications`, `scripts`, `modem`, `config`, `power`, `audio`, `notifications`, `wm`, `timezone`, `games`, `st`, `nano`, `sthotkeys`) and `fcitx5-t9` XIM input.
|
||||
- `src/dwm-plugins/`: Preloaded C++20 `240x320` DWM plugins and shared headers:
|
||||
- `theme.h`: Unified `.Xresources` / `~/.xthemes/*.xth` parser, proportional font sizing (`theme.baseFontSize`), automatic shade derivation, and live Xft/Pango theme reloading (`PluginTheme`).
|
||||
- `phone_data.h`: Shared X11 geometry (`240x296` below the 24px bar), FIFO IPC (`/tmp/dwm_plugin_<name>.fifo`), softkey chord detection, Sxmo TSV contact/call/SMS parsers, and `libjpeg`/`libpng` helpers.
|
||||
- `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `callscreen_plugin.cpp` (`callscreen.so` / `xcallscreen`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`).
|
||||
- `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcallscreen`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `dmenu`, and `xdmenu`.
|
||||
- `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `callscreen_plugin.cpp` (`callscreen.so` / `xcallscreen`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`), `modem_plugin.cpp` (`modem.so` / `xmodem`), `networks_plugin.cpp` (`networks.so` / `xnetworks` / `xbluetooth`).
|
||||
- `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcallscreen`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `xmodem`, `xnetworks`, `xbluetooth`, `dmenu`, and `xdmenu`.
|
||||
- `src/xdrawer/`: Global phone controls overlay (`xdrawer.so` / `xdrawer`), toggled by pressing both softkeys simultaneously.
|
||||
- `src/sxmo-st/`: Patched `st` terminal with HarfBuzz OpenType ligatures (`Fira Code`), `XIM` (`fcitx5-t9`), and live `SIGUSR1` `.Xresources` reload.
|
||||
- `src/fcitx5-t9/`: 6-mode keypad input method (`abc`, `Abc`, `ABC`, `Sym`, `Ctl`, `123` + `*` long-press `Ctrl+` latch) with fixed bottom preedit bar (`Y=290, H=30`).
|
||||
- `src/games/`: Standalone keypad-driven `240x320` X11 games (`snake.cpp` -> `/usr/bin/snake`, `tetris.cpp` -> `/usr/bin/tetris`) with built-in 5x7 bitmap fonts, `timerfd`-driven pacing, and offscreen `Pixmap` double buffering.
|
||||
- `src/msm8905-cam/` & `src/leds-msm8905-torch/`: Out-of-tree Linux `6.12.1-msm8916` kernel modules for the rear `GC2395` camera (`/dev/video0`) and `SY7807` torch LED (`white:torch`) / WCD codec init / PM8909 `PON` shutdown.
|
||||
- `overlay/rootfs/`: System configuration overlay applied over the target rootfs (kept in sync with `prebuilt/`).
|
||||
- `prebuilt/`: Cross-compiled `armv7` binaries and kernel modules (`sxmo-x11`, `fcitx5-t9`, `xlibre`, `kernel-6.12.1-msm8916`, `bantam`).
|
||||
- `prebuilt/`: Cross-compiled `armv7` binaries and kernel modules (`sxmo-x11`, `fcitx5-t9`, `xlibre`, `kernel-6.12.1-msm8916`, `bantam`, `games`).
|
||||
- `devices/` & `patches/`: Device trees (`.dts`/`.dtb`), MIPI-DBI panel init sequences, and upstream patch sets.
|
||||
|
||||
## 3. Build & Image Generation Commands
|
||||
All cross-compilation scripts run inside rootless Docker (`alpine:3.24` `armv7` sysroot) and automatically update both `prebuilt/` and `overlay/rootfs/`:
|
||||
- `./scripts/build-sxmo-x11.sh` — Builds `st`, `dmenu.so`, `dwm`, `dwm-plugin`, and all C++ `.so` plugins (`xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`).
|
||||
- `./scripts/build-sxmo-x11.sh` — Builds `st`, `dmenu.so`, `dwm`, `dwm-plugin`, and all C++ `.so` plugins (`xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`, `modem`, `networks`).
|
||||
- `./scripts/build-dmenu.sh` — Rebuilds only `dmenu.so`, `stest`, `dmenu_path`, and `dmenu_run`.
|
||||
- `./scripts/build-games.sh` — Builds standalone X11 games in `src/games/` (`snake`, `tetris`).
|
||||
- `./scripts/build-fcitx5-t9.sh` — Builds `src/fcitx5-t9` (`libt9.so` / `t9.so`).
|
||||
- `./scripts/build-kernel-audio.sh` — Builds `6.12.1-msm8916` QDSP6 audio, torch LED, camera `.ko` modules, and device `.dtb` files.
|
||||
- `./scripts/build-xlibre.sh` — Builds Xlibre `Xorg` and `libinput`/`fbdev` DDX drivers.
|
||||
|
||||
@@ -29,7 +29,7 @@ This manual covers flashing, operating, and customizing **Bananarchy Linux** (Sx
|
||||
- **Patched `sxmo-st` (`st`) with HarfBuzz ligatures & XIM**: patched with `libharfbuzz` (`hb.c`/`hb.h`) shaping (`liga`, `calt`, `clig`, `dlig`) so multi-character programming operators (`->`, `=>`, `!=`, `<=`, `>=`, `===`, `::`, `//`, `/*`) render as continuous ligatures at `pixelsize=18`, alongside full `XIM` (`fcitx5-t9`) input support.
|
||||
- **Patched `sxmo-dmenu` plugin (`dmenu.so` / `dmenu` / `xdmenu`) (`< 2 ms` popup/close)**: preloaded resident DWM plugin rendering a full-screen `240x296` 10-line vertical menu (positioned flush at `Y = 24..320` right below the 24px `dwm` bar, with a `26px` search bar and ten `27px` item rows) with a `#383838` top search bar, `#005577` prompt badge, `#1c1c1c` item list, `XIM` (`XSetLocaleModifiers("")`, `XSetICFocus`, `Xutf8LookupString`) for live `fcitx5-t9` search filtering and `#` mode switching, C-level focus preservation (`prevfocuswin`), built-in `< 0.5 ms` in-place Sxmo menu transitions (`Sys`, `Apps`, `Scripts`, `Modem`, `Config`, `Power`, `St`, `Nano`), and a `/tmp/dwm_plugin_dmenu.sock` IPC server for all `/usr/bin/dmenu` and `sxmo_dmenu.sh` calls.
|
||||
- **Patched `sxmo-dwm` (`dwm`) (`< 2 ms` display wake/unlock & native plugin system)**: configured for `240x320` (`bh = 24`, `#323232` top bar, `#005577` active tag/border, vertical `bstack` layout default, overflow-protected Pango status bar rendering keeping the right-aligned clock always visible, and a transparent-backgrounded bottom desktop softkey bar displaying `"Menu"` and `"Names"` over the wallpaper). Directly polls `/dev/input/by-path/platform-gpio-keys-event` (`SW_LID`) and intercepts locked-state wake keys (`XF86PowerOff`, `XF86PickupPhone`, `XF86KbdLcdMenu1`, `XF86KbdLcdMenu2`) in C to toggle `/sys/class/backlight/backlight/brightness` and `/sys/class/leds/white:kbd_backlight/brightness` in `< 2 ms` before running Sxmo state hooks asynchronously.
|
||||
- **Preloaded DWM plugins (`src/sxmo-dmenu/`, `src/dwm-plugins/` & `src/xdrawer/`, `< 2 ms` FIFO/socket wakeup & unified `.Xresources` theming)**: `dwm` dynamically loads `.so` plugins from `/usr/lib/dwm/plugins/` and `~/.local/lib/dwm/plugins/` (`dmenu.so`, `xdrawer.so`, `dialer.so`, `calllog.so`, `callscreen.so`, `contacts.so`, `sms.so`, `files.so`, `camera.so`, `themes.so`) and preloads them as resident background daemons (`dwm-plugin <name> --preload`) at startup. Each daemon keeps its X11 window, offscreen `Pixmap`, Xft/Pango layouts, and `stat()`-cached Sxmo TSV state initialized in memory while sleeping at 0% CPU on `/tmp/dwm_plugin_<name>.fifo`. Every UI element across `dwm`, `dmenu`, `st`, and all C++ plugins obeys a single unified color and font scheme configured via `~/.Xresources` (`theme.*`, `dwm.*`, `dmenu.*`, `st.*`), with proportional font sizing derived from `theme.baseFontSize` (`12px` / `15px` / `16px` / `18px` at default `15`) and instant live reload via `xthemes`.
|
||||
- **Preloaded DWM plugins (`src/sxmo-dmenu/`, `src/dwm-plugins/` & `src/xdrawer/`, `< 2 ms` FIFO/socket wakeup & unified `.Xresources` theming)**: `dwm` dynamically loads `.so` plugins from `/usr/lib/dwm/plugins/` and `~/.local/lib/dwm/plugins/` (`dmenu.so`, `xdrawer.so`, `dialer.so`, `calllog.so`, `callscreen.so`, `contacts.so`, `sms.so`, `files.so`, `camera.so`, `themes.so`, `modem.so`, `networks.so`) and preloads them as resident background daemons (`dwm-plugin <name> --preload`) at startup. Each daemon keeps its X11 window, offscreen `Pixmap`, Xft/Pango layouts, and `stat()`-cached Sxmo TSV state initialized in memory while sleeping at 0% CPU on `/tmp/dwm_plugin_<name>.fifo`. Every UI element across `dwm`, `dmenu`, `st`, and all C++ plugins obeys a single unified color and font scheme configured via `~/.Xresources` (`theme.*`, `dwm.*`, `dmenu.*`, `st.*`), with proportional font sizing derived from `theme.baseFontSize` (`12px` / `15px` / `16px` / `18px` at default `15`) and instant live reload via `xthemes`.
|
||||
2. **Multi-mode `fcitx5-t9` keypad engine (`src/fcitx5-t9/`)**:
|
||||
- Supports 6 input modes cycled with `#` (`XF86NumericPound`): `abc`, `Abc`, `ABC`, `Sym`, `Ctl`, and `123`, updating the live top-bar status indicator (`[abc]`, `[Abc]`, `[ABC]`, `[Sym]`, `[Ctl]`, `[123]`) in real time.
|
||||
- Locks the candidate popup bar to the bottom of the screen (`X = 1, Y = 290, W = 238, H = 30`) with fixed-width 28-character monospace formatting (`Fira Code 9.5`), so neither the popup window nor individual letters ever jump when cycling through candidates.
|
||||
@@ -141,17 +141,17 @@ You can switch between **vertical stack (`TTT`, `bstack`)**, **horizontal tile (
|
||||
- `Mod4+1`..`4`: switch to workspace tag `1`..`4`
|
||||
- `Mod4+Shift+q`: close focused window
|
||||
|
||||
#### 3.1.2 Preloaded DWM plugins (`DMenu`, `XDrawer`, `Dialer`, `Call Log`, `Call Screen`, `Contacts`, `SMS`, `Files`, `Camera`, `Themes`)
|
||||
All ten core phone overlays are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup (`dwm-plugin <name> --preload` with an exclusive `/tmp/dwm_plugin_<name>.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_<name>.fifo` in `< 2 ms`:
|
||||
#### 3.1.2 Preloaded DWM plugins (`DMenu`, `XDrawer`, `Dialer`, `Call Log`, `Call Screen`, `Contacts`, `SMS`, `Files`, `Camera`, `Themes`, `Modem`, `Networks`)
|
||||
All twelve core phone overlays are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup (`dwm-plugin <name> --preload` with an exclusive `/tmp/dwm_plugin_<name>.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_<name>.fifo` in `< 2 ms`:
|
||||
|
||||
1. **`DMenu` (`dmenu.so` / `dmenu` / `xdmenu`) — Zero-Lag System & Context Menus**:
|
||||
- **Open / Close**: Press **Left softkey (`Menu`)** for the **System menu** (`sys`) or **Right softkey (`Back`)** inside an application for its **Context menu** (`ctx`); press the same softkey or `Escape` to close.
|
||||
- **Built-in Native Menus & `< 0.5 ms` In-Place Transitions**: Renders `sys`, `ctx`, `applications` (including **Nano**, **Bantam**, **Files**, **Camera**, **Dialer**, **Contacts**), `scripts`, `modem`, `config`, `power` (with Polkit + `elogind` multi-session **Reboot** and **Poweroff** via `sxmo_power.sh`), `st`, `nano`, `sthotkeys`, and confirmation prompts directly inside the resident `240x296` plugin window without spawning shell scripts or new X11 connections. Submenu transitions (`@menu:...`) and hardware toggles (`@toggle:flashlight`, `@toggle:brightness`, `@toggle:autosuspend`, `@toggle:autocallscreenoff`, `@toggle:bluetooth`) update in place in `< 0.5 ms`.
|
||||
- **Built-in Native Menus & `< 0.5 ms` In-Place Transitions**: Renders `sys`, `ctx`, `applications` (including **Nano**, **Bantam**, **Files**, **Camera**, **Dialer**, **Contacts**), `scripts`, `modem`, `config`, `power` (with Polkit + `elogind` multi-session **Reboot** and **Poweroff** via `sxmo_power.sh`), `audio`, `notifications`, `wm`, `timezone`, `games`, `st`, `nano`, `sthotkeys`, and confirmation prompts directly inside the resident `240x296` plugin window without spawning shell scripts or new X11 connections. Submenu transitions (`@menu:...`) and hardware toggles (`@toggle:flashlight`, `@toggle:brightness`, `@toggle:autosuspend`, `@toggle:autocallscreenoff`, `@toggle:bluetooth`, `@toggle:dnd`) update in place in `< 0.5 ms`.
|
||||
- **Client Socket Mode (`/tmp/dwm_plugin_dmenu.sock`)**: Any script piping items into `/usr/bin/dmenu` or `sxmo_dmenu.sh` streams its item list over the Unix domain socket to the resident `dmenu.so` daemon and receives the user's selection instantaneously, while `/tmp/dwm_plugin_dmenu.active` provides zero-overhead `sxmo_dmenu.sh isopen` checks.
|
||||
|
||||
2. **`XDrawer` (`xdrawer.so` / `xdrawer`) — Global Phone Controls**:
|
||||
- **Open / Close**: Press **both softkeys simultaneously** (`KEY_MENU` + `KEY_BACK`) from anywhere, or select `XDrawer` from the Sxmo menu.
|
||||
- **Controls**: `Up`/`Down` selects a row (**Speaker Volume**, **Earpiece Volume**, **Flashlight**, **Sound**, **Microphone**, **Screen Brightness**, **Keypad Backlight**); `Left`/`Right` adjusts sliders or toggles; `Return` flips toggles; `1`–`7` jumps directly to row `1`–`7`; `Escape` or both softkeys closes the drawer.
|
||||
- **Controls**: `Up`/`Down` selects a row (**Speaker Volume**, **Earpiece Volume**, **Ringer Mode** [`Ring+Vib` / `Vibrate` / `Silent` / `DND`], **Flashlight**, **Sound**, **Microphone**, **Screen Brightness**, **Keypad Backlight**); `Left`/`Right` adjusts sliders or cycles modes/toggles; `Return` flips toggles; `1`–`8` jumps directly to row `1`–`8`; `Escape` or both softkeys closes the drawer.
|
||||
|
||||
3. **`Dialer` (`dialer.so` / `xdialer`) — Keypad Dialer with Contact Integration**:
|
||||
- **Open**: On the empty desktop, press any **`0`–`9`, `*`, or `#` key** (which opens the dialer and immediately inserts that character), or launch **Dialer** from the Sxmo menu or Call Log.
|
||||
@@ -201,6 +201,36 @@ All ten core phone overlays are compiled as shared-object plugins (`/usr/lib/dwm
|
||||
- **Controls & Live Switching**: `Up`/`Down` browses installed themes with live color swatches and typography details; **Center `OK` (`Return`)**, **Call (green)**, or keys **`1`–`9`** immediately writes `~/.Xresources`, runs `xrdb -load ~/.Xresources`, and broadcasts `RELOAD_THEME` to `dwm`, `dmenu`, `st`, and all resident DWM plugins for zero-restart live theme switching.
|
||||
- **Built-in Theme Customizer & Options (`Left softkey` or `*`/`#`)**: Supports **Customize Theme Colors/Font** (`*` shortcut — adjust `Font Face`, `Base Size`, `Main BG`, `Bar BG`, `Header BG`, `Selected BG`, `Selected FG`, `Accent`, `Font Color`, and `Muted Color` with `Left`/`Right` presets or press `Return` to enter any 6-digit `#RRGGBB` hex code via the keypad), **Save Current as New Theme** (`#` shortcut), **Duplicate Theme**, **Edit `.xth` in Nano**, **Reset Default Theme**, and **Delete Theme**, plus CLI `xthemes --apply <ThemeName>` and `xthemes --export-default`.
|
||||
|
||||
11. **`Modem, Dual-SIM & PIN/PUK Manager` (`modem.so` / `xmodem`) — Dual-SIM Slot Provisioning, PIN/PUK Entry & Radio Controls**:
|
||||
- **Open**: Select **Modem** from the Sxmo **System menu** (`Left softkey`), or run `xmodem` (`xmodem unlock`, `xmodem info`, `xmodem log`, `xmodem ussd`). Also opens automatically when the active SIM card is PIN- or PUK-locked (`sxmo_hook_modem.sh`).
|
||||
- **Tabs (`Left`/`Right` or `1` / `2` / `3`)**:
|
||||
- **`1:SIMs` (Dual-SIM & PIN/PUK Management)**: Inspects both physical Qualcomm UIM card slots (`qmicli -pd qrtr://0 --uim-get-card-status` + `/usr/libexec/msm-modem-uim-selection status`), displaying each slot's presence (`PRESENT` / `EMPTY`), active `Primary GW` provisioning badge (`[ACTIVE]`), lock state (`READY`, `PIN-LOCK`, `PUK-LOCK`), remaining PIN1/PUK1 retries, operator, and ICCID. Selecting a slot (`Return` or `Left softkey`) opens the **SIM Slot Actions** menu:
|
||||
- **Activate SIM Slot 1 / 2**: Switches Qualcomm QMI `primary-gw-provisioning` to the chosen physical SIM slot, persists `preferred_sim_slot` in `/etc/conf.d/msm-modem-uim-selection`, and restarts `ModemManager`.
|
||||
- **Unlock SIM PIN**: Masked numeric PIN dialog (`4`–`8` digits, `*` toggles show/hide digits) with live remaining retry counter.
|
||||
- **Unblock SIM with PUK**: Guided 3-step wizard (`8`-digit PUK1 -> new `4`–`8` digit PIN -> confirm new PIN).
|
||||
- **Enable / Disable PIN Lock**: Toggles SIM PIN verification requirement on power-up.
|
||||
- **Change SIM PIN**: Guided 3-step wizard (Current PIN -> New PIN -> Confirm New PIN).
|
||||
- **`2:Radio` (Modem Controls & USSD)**: Displays live signal strength (`%` and visual bar), registration state, access technology (`LTE` / `UMTS` / `GSM`), operator name, IMEI, and phone number, plus interactive rows to **Toggle Modem Monitor**, switch **Network Mode** (`4G/3G/2G`, `3G/2G`, `4G Only`, `3G Only`, `2G Only`), **Send USSD Code** (with keypad entry for `*100#`, etc., and live USSD response display), and **Restart ModemManager**.
|
||||
- **`3:Log` (Modem Log)**: Color-coded history of calls, SMS events, and USSD responses from `$SXMO_LOGDIR/modemlog.tsv`.
|
||||
|
||||
12. **`Networks & Bluetooth Manager` (`networks.so` / `xnetworks` / `xbluetooth`) — Wi-Fi, Cellular APN, Tethering & Bluetooth**:
|
||||
- **Open**: Select **Networks** or **Bluetooth** from the Sxmo **System menu** (`Left softkey`), or run `xnetworks` / `xbluetooth`.
|
||||
- **Tabs (`Left`/`Right` or `1` / `2` / `3`)**:
|
||||
- **`1:Wi-Fi`**: Toggle Wi-Fi radio ON/OFF, scan (`*` or **Rescan Networks**), toggle **Wi-Fi Hotspot**, and browse saved/nearby SSIDs with signal bars, security badges (`WPA2`/`OPEN`), and `[CONNECTED]` status. Selecting an SSID opens **Connect / Switch**, **Enter Password** (multi-tap `abc`/`Abc`/`ABC`/`123`/`Sym` keypad modal with `*` password visibility toggle), **Disconnect**, or **Forget Network**.
|
||||
- **`2:Cell/APN`**: Toggle **Mobile Data** (`sxmo_modemdata.sh`), view current cellular IPv4 address and bearer interface (`wwan0`), edit **APN Configuration** (`APN Name`, `Username`, `Password`, and **Save & Activate APN**), and toggle **USB Tethering**.
|
||||
- **`3:Bluetooth`**: Toggle Bluetooth controller power (`doas rfkill unblock bluetooth` + `bluetoothctl power on/off`), scan for nearby devices (`*` or **Scan Nearby (`8s`)**), and manage paired/discovered devices (**Connect**, **Disconnect**, **Pair & Trust**, **Forget / Remove**).
|
||||
|
||||
#### 3.1.3 Standalone Keypad Games (`Snake` & `Tetris`)
|
||||
Two classic monochrome/Gameboy-styled X11 games (`src/games/snake.cpp` -> `/usr/bin/snake`, `src/games/tetris.cpp` -> `/usr/bin/tetris`) are included in the **Games** submenu (`Left softkey -> Games`):
|
||||
|
||||
1. **`Snake` (`snake`)**:
|
||||
- **Controls**: `2` / `Up`, `8` / `Down`, `4` / `Left`, `6` / `Right` to steer (with a 2-turn input queue and immediate mid-step turn response); `5` / `Return` / `#` / `*` / `P` to pause, resume, or start a new game after Game Over; `BackSpace` / `Right softkey` / `Escape` / `Q` to quit.
|
||||
- **Gameplay**: Wrap-around playfield edges (`24x28` grid) with progressive speed scaling (`+1 px/s` per 5 food eaten) and automatic pause on focus loss.
|
||||
|
||||
2. **`Tetris` (`tetris`)**:
|
||||
- **Controls**: `4` / `Left` and `6` / `Right` to move horizontally; `2` / `Up` to rotate clockwise (with wall kicks); `8` / `Down` for soft drop; `0` / `5` / `Return` / `Space` for hard drop (or resume/new game when paused/game over); `#` / `*` / `P` to pause/resume; `BackSpace` / `Right softkey` / `Escape` / `Q` to quit.
|
||||
- **Gameplay**: 4-shade beveled Gameboy palette, next-piece preview box, score/lines/level/best stats, progressive gravity scaling (`+1 px/s` per 10 lines cleared), and automatic pause on focus loss.
|
||||
|
||||
### 3.2 Multi-mode `fcitx5-t9` keypad reference
|
||||
|
||||
- **Short-press `#`** *(when no character is in preedit)*: cycles through the 6 input modes (`abc` -> `Abc` -> `ABC` -> `Sym` -> `Ctl` -> `123` -> `abc`), displaying a `1500ms` preedit status banner (`[Mode: abc]`, `[Mode: Abc]`, `[Mode: ABC]`, `[Mode: Sym]`, `[Mode: Ctl]`, `[Mode: 123]`).
|
||||
|
||||
@@ -10,9 +10,9 @@ Bananarchy targets an ultra-lightweight **Sxmo X11 (`sxmo-de-dwm`)** environment
|
||||
|
||||
- **Cross-compiled Xlibre (`X11Libre/xserver` 25.2.0) display server**: `modesetting` KMS + `ShadowFB` Damage partial-rectangle SPI transfers, software cursor fallback, and rootless `elogind` integration.
|
||||
- **Patched `sxmo-dwm` with native C/C++ plugin architecture**: `240x320` vertical `bstack` layout, 24px Pango status bar with an overflow-protected right-aligned clock, transparent-backgrounded `"Menu"` and `"Names"` desktop softkey labels, direct C `SW_LID` slider and locked-key wake handling in `< 2 ms`, and a dynamic `.so` plugin API (`dwm_plugin.h`) with preloaded resident daemons (`--preload`) and non-blocking FIFO IPC (`< 10 ms` wakeup latency).
|
||||
- **Preloaded `240x320` keypad-driven DWM plugins (`dmenu`, `xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`)**:
|
||||
- **`DMenu` (`dmenu.so` / `dmenu` / `xdmenu`)**: zero-lag (`< 2 ms` popup/close) resident `240x320` full-screen 10-line vertical menu (`240x296` flush below the 24px top bar) with a `#383838` search bar, `#005577` prompt badge, `XIM` `fcitx5-t9` text input, in-place `< 0.5 ms` native Sxmo submenu transitions (`Sys`, `Apps`, `Scripts`, `Modem`, `Config`, `Power`, `St`, `Nano`), and a `/tmp/dwm_plugin_dmenu.sock` client interface serving all `/usr/bin/dmenu` and `sxmo_dmenu.sh` pipes instantaneously.
|
||||
- **`XDrawer` (`xdrawer.so` / `xdrawer`)**: global phone controls overlay invoked from anywhere by pressing **both softkeys simultaneously**, providing instant sliders and toggles for speaker volume, earpiece volume, flashlight, sound mute, microphone mute, screen brightness, and keypad backlight.
|
||||
- **Preloaded `240x320` keypad-driven DWM plugins (`dmenu`, `xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`, `modem`, `networks`)**:
|
||||
- **`DMenu` (`dmenu.so` / `dmenu` / `xdmenu`)**: zero-lag (`< 2 ms` popup/close) resident `240x320` full-screen 10-line vertical menu (`240x296` flush below the 24px top bar) with a `#383838` search bar, `#005577` prompt badge, `XIM` `fcitx5-t9` text input, in-place `< 0.5 ms` native Sxmo submenu transitions (`Sys`, `Apps`, `Scripts`, `Modem`, `Config`, `Power`, `Audio`, `Notifications`, `WM`, `Timezone`, `Games`, `St`, `Nano`), and a `/tmp/dwm_plugin_dmenu.sock` client interface serving all `/usr/bin/dmenu` and `sxmo_dmenu.sh` pipes instantaneously.
|
||||
- **`XDrawer` (`xdrawer.so` / `xdrawer`)**: global phone controls overlay invoked from anywhere by pressing **both softkeys simultaneously**, providing instant sliders and toggles for speaker volume, earpiece volume, ringer mode (`Ring+Vib` / `Vibrate` / `Silent` / `DND`), flashlight, sound mute, microphone mute, screen brightness, and keypad backlight.
|
||||
- **`Dialer` (`dialer.so` / `xdialer`)**: opens automatically from the idle desktop when pressing any **`0`–`9`, `*`, or `#` key** (inserting that character immediately), with live T9/number contact filtering, hold-`0` for `+`, and single-press **Call** dialing.
|
||||
- **`Call Log` (`calllog.so` / `xcalllog`)**: opens automatically from the idle desktop when pressing the **Call (green)** button, with `All` / `Missed` / `In` / `Out` tabs, contact resolution, call durations, and instant redial.
|
||||
- **`Call Screen` (`callscreen.so` / `xcallscreen`)**: dedicated incoming, outgoing, and in-call screen integrated with Sxmo's `ModemManager` call lifecycle (`sxmo_modem.sh`, `sxmo_modemcall.sh`, `sxmo_modemdial.sh`), displaying resolved caller name and phone number, accepting/rejecting incoming calls via physical **Call (green)** and **End (red)** buttons (plus `*`/`0` ringtone silence), live `MM:SS` call timer, microphone mute/unmute (`1`/`*`), live call recording to `~/Recordings/call_<caller>_<timestamp>.wav` (`2`/`#`), earpiece/speaker audio routing (`3`), volume control (`Left`/`Right`), and DTMF tone keypad mode (`0` / `Left softkey`).
|
||||
@@ -20,6 +20,8 @@ Bananarchy targets an ultra-lightweight **Sxmo X11 (`sxmo-de-dwm`)** environment
|
||||
- **`SMS Manager` (`sms.so` / `xsms`)**: conversation thread manager, bubble message history reader, and multi-tap keypad SMS composer/draft editor (with dynamic `Send` / `Draft` center softkey action) integrated with `$SXMO_LOGDIR/<number>/sms.txt`, `draft.txt`, `modemlog.tsv`, and `sxmo_modemsendsms.sh`.
|
||||
- **`File Manager` (`files.so` / `xfiles`)**: graphical directory browser with live T9/number filtering, dotfiles toggle (`*`), sort mode cycle (`#`), file operations (new folder/file, rename, copy, cut/move, paste, delete, properties, edit in `nano`, open terminal here), built-in text viewer, and built-in `libjpeg`/`libpng` image previewer.
|
||||
- **`Camera` (`camera.so` / `xcamera`)**: live V4L2 `/dev/video0` (`800x600` `RGB24` -> `240x180` 4:3) viewfinder with 1-second frame watchdog, auto-flash (`*`/`1`), continuous torch (`0`/`2`), digital zoom (`1x`/`2x`/`4x` via `Up`/`Down`), 3-second self-timer (`#`/`3`), direct `libjpeg` photo capture to `~/Pictures/photo-YYYYMMDD-HHMMSS.jpg`, and built-in photo gallery.
|
||||
- **`Modem, Dual-SIM & PIN/PUK Manager` (`modem.so` / `xmodem`)**: 3-tab (`1:SIMs`, `2:Radio`, `3:Log`) cellular and Dual-SIM manager inspecting both physical Qualcomm UIM slots (`qmicli -pd qrtr://0 --uim-get-card-status` + `mmcli`), supporting active SIM slot switching (`primary-gw-provisioning` between Slot 1 and Slot 2 persisted in `/etc/conf.d/msm-modem-uim-selection`), masked keypad SIM PIN unlock (`4`–`8` digits), 3-step PUK unblock wizard (`8`-digit PUK + new PIN + confirm), PIN lock enable/disable and PIN change, 2G/3G/4G radio mode switching, interactive USSD session dialog (`*100#`, etc.), and color-coded modem event log viewer.
|
||||
- **`Networks & Bluetooth Manager` (`networks.so` / `xnetworks` / `xbluetooth`)**: 3-tab (`1:Wi-Fi`, `2:Cell/APN`, `3:Bluetooth`) connectivity manager providing Wi-Fi radio toggle, visual signal bars, multi-tap Wi-Fi WPA/WPA2 password dialog (`*` show/hide password), Wi-Fi Hotspot and USB Tethering toggles, cellular data (`sxmo_modemdata.sh`) and inline APN editor (`Name`, `Username`, `Password`), and Bluetooth controller power, scanning, pairing, and audio/device connection management.
|
||||
- **`Theme Manager` (`themes.so` / `xthemes`)**: unified `.Xresources` theme browser and live customizer managing `~/.xthemes/*.xth` (where every `.xth` file is a valid `Xresources` file controlling `theme.*`, `dwm.*`, `dmenu.*`, `st.*`, and `svkbd.*` colors, font face, and `baseFontSize` with proportional derived font sizes, pre-populated with 20 themes: `Default.xth`, `BananaGold.xth`, `CatppuccinLatte.xth`, `CatppuccinMocha.xth`, `Cyberpunk.xth`, `Dracula.xth`, `EverforestDark.xth`, `GitHubDark.xth`, `GruvboxDark.xth`, `GruvboxLight.xth`, `Kanagawa.xth`, `LinuxConsole.xth`, `MatrixGreen.xth`, `MonokaiPro.xth`, `NordFrost.xth`, `OneDark.xth`, `RosePine.xth`, `SolarizedDark.xth`, `SolarizedLight.xth`, and `TokyoNight.xth`). Applying or editing a theme reloads `dwm`, `dmenu`, `st`, and all resident DWM plugins live without restarting X11.
|
||||
- **Patched `sxmo-st` terminal (`pixelsize=18`)**: **HarfBuzz (`libharfbuzz`) OpenType ligature shaping** (`liga`, `calt`, `clig`, `dlig`) and `XIM` support.
|
||||
- **Typography**: non-Nerd **`Fira Code` (`font-fira-code`) with ligatures** as the primary system and terminal font (with `DejaVu SansM Nerd Font` used strictly as an icon fallback).
|
||||
@@ -28,6 +30,7 @@ Bananarchy targets an ultra-lightweight **Sxmo X11 (`sxmo-de-dwm`)** environment
|
||||
- **Full Qualcomm QDSP6/Q6Voice audio & modem routing**: loudspeaker, earpiece speaker, and internal/headset microphones with PM8909 WCD analog codec 2.8V micbias, +6 dB EAR PA gain, and GSM/WCDMA/LTE voice routing (`ModemManager` + `q6voiced`).
|
||||
- **Rear 2MP camera support**: `msm8905-cam` V4L2 driver for GalaxyCore `GC2395` + MSM8909 `CAMSS` on `/dev/video0`, native `xcamera` DWM plugin, and the `bananarchy-photo` CLI utility.
|
||||
- **Pre-integrated Bantam AI agent**: cross-compiled from `https://code.luxferre.top/luxferre/bantam-cpp` and integrated into the Sxmo **Apps** menu (`Applications -> Bantam`, launching in an `st` terminal with `$HOME` as its default working directory).
|
||||
- **Built-in keypad X11 games (`Snake` & `Tetris`)**: standalone zero-dependency X11 games (`src/games/snake.cpp`, `src/games/tetris.cpp`) integrated into the Sxmo **Games** menu (`System Menu -> Games`), featuring Nokia green / Gameboy beveled aesthetics, batched `XFillRectangles` rendering, and `timerfd`-driven pacing.
|
||||
- **Complete on-device development & scripting stack**: `git`, `gcc`/`g++`, `make`, GNU `coreutils`, `nano` (`$EDITOR`, pre-configured in `/etc/nanorc` with `-FEDSMAGic/max%_T2`), `ed`, `less` (`$PAGER="less -RXF"`), `rlwrap`, `frotz` (Z-machine interactive fiction interpreter), `janet` + `jpm`, `perl` + `IO::Socket::SSL`, `tcl` + `tcllib`/`tcl-tls`/`sqlite-tcl`, `sqlite`, `i2c-tools`, and `v4l-utils`.
|
||||
|
||||
---
|
||||
@@ -93,20 +96,22 @@ bananarchy/
|
||||
│ └── 0001-bananarchy-multimode-t9-and-fcitx5-5.1.19.patch
|
||||
├── src/
|
||||
│ ├── sxmo-dwm/ # Patched sxmo-dwm (240x320 bstack, Pango bar, desktop softkey bar & plugin API)
|
||||
│ ├── dwm-plugins/ # DWM plugins (dialer, calllog, callscreen, contacts, sms, files, camera, themes), theme.h & runner
|
||||
│ ├── dwm-plugins/ # DWM plugins (dialer, calllog, callscreen, contacts, sms, files, camera, themes, modem, networks), theme.h & runner
|
||||
│ ├── xdrawer/ # XDrawer global phone controls DWM plugin (xdrawer.so / xdrawer)
|
||||
│ ├── sxmo-dmenu/ # Patched sxmo-dmenu zero-lag DWM plugin (dmenu.so) & client/fallback (dmenu / xdmenu)
|
||||
│ ├── sxmo-st/ # Patched sxmo-st terminal with HarfBuzz Fira Code ligature shaping & XIM
|
||||
│ ├── xf86-input-libinput/ # X11Libre xf86-input-libinput DDX driver
|
||||
│ ├── xf86-video-fbdev/ # X11Libre xf86-video-fbdev fallback DDX driver
|
||||
│ ├── fcitx5-t9/ # C++20 source of Bananarchy's 6-mode fcitx5-t9 input method
|
||||
│ ├── games/ # Standalone keypad X11 games (snake.cpp, tetris.cpp)
|
||||
│ ├── leds-msm8905-torch/ # Kernel driver for SY7807 rear torch LED, WCD codec init & PM8909 PON shutdown
|
||||
│ └── msm8905-cam/ # Kernel V4L2 driver for GC2395 2MP rear camera & MSM8909 CAMSS (/dev/video0)
|
||||
├── overlay/
|
||||
│ └── rootfs/ # Complete Bananarchy rootfs overlay (OS branding, Sxmo X11, audio, camera, T9, .xthemes)
|
||||
├── prebuilt/
|
||||
│ ├── xlibre/ # Prebuilt armv7 Xlibre Xorg binary and modesetting/libinput/fbdev DDX modules
|
||||
│ ├── sxmo-x11/ # Prebuilt armv7 patched dwm, dmenu.so/dmenu, 9 C++ DWM plugins, and st (HarfBuzz ligatures)
|
||||
│ ├── sxmo-x11/ # Prebuilt armv7 patched dwm, dmenu.so/dmenu, 11 C++ DWM plugins, and st (HarfBuzz ligatures)
|
||||
│ ├── games/ # Prebuilt armv7 standalone X11 games (snake, tetris)
|
||||
│ ├── kernel-6.12.1-msm8916/ # Prebuilt QDSP6/Q6Voice .ko, torch .ko, camera .ko, and 5 device DTBs
|
||||
│ ├── fcitx5-t9/ # Prebuilt armv7 libt9.so and fcitx5 addon/inputmethod configs
|
||||
│ └── bantam/ # Prebuilt armv7 bantam AI agent binary
|
||||
@@ -115,6 +120,8 @@ bananarchy/
|
||||
├── mipi-dbi-cmd.py # Compiles MIPI-DBI panel .txt init sequences into .bin firmware
|
||||
├── build-xlibre.sh # Cross-compiles Xlibre + xf86-input-libinput + xf86-video-fbdev for armv7 Cortex-A7
|
||||
├── build-sxmo-x11.sh # Cross-compiles sxmo-st (HarfBuzz), sxmo-dmenu (dmenu.so + dmenu), sxmo-dwm & DWM plugins
|
||||
├── build-dmenu.sh # Cross-compiles sxmo-dmenu (dmenu.so + stest) standalone
|
||||
├── build-games.sh # Cross-compiles standalone X11 games (snake, tetris) for armv7
|
||||
├── build-kernel-audio.sh # Cross-compiles Linux 6.12.1-msm8916 QDSP6/torch/camera modules & DTBs
|
||||
├── build-fcitx5-t9.sh # Cross-compiles src/fcitx5-t9 for armv7
|
||||
├── build-bantam.sh # Fetches master from https://code.luxferre.top/luxferre/bantam-cpp & cross-compiles for armv7
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Maximum time to wait for SIM card to appear after starting the modem.
|
||||
# Some SIM cards or modem firmwares might take longer to initialize. :(
|
||||
# To avoid blocking boot without a SIM card for too long this is fairly short
|
||||
# by default (4 seconds), but you can increase it to e.g. 30 seconds if necessary.
|
||||
# Some SIM cards or modem firmwares might take longer to initialize.
|
||||
# Set to 0 to disable waiting entirely.
|
||||
sim_wait_time=4
|
||||
|
||||
# Preferred physical SIM slot (1 or 2) for Primary GW provisioning on Dual-SIM
|
||||
# Qualcomm MSM8905/MSM8909 devices. Falls back automatically if the preferred
|
||||
# slot has no SIM card inserted.
|
||||
preferred_sim_slot=1
|
||||
|
||||
@@ -16,10 +16,16 @@ permit nopass :wheel as root cmd rc-service args eg25-manager stop
|
||||
permit nopass :wheel as root cmd rc-service args eg25-manager start
|
||||
permit nopass :wheel as root cmd rc-service args modemmanager stop
|
||||
permit nopass :wheel as root cmd rc-service args modemmanager start
|
||||
permit nopass :wheel as root cmd rc-service args modemmanager restart
|
||||
permit nopass :wheel as root cmd systemctl args start eg25-manager
|
||||
permit nopass :wheel as root cmd systemctl args stop eg25-manager
|
||||
permit nopass :wheel as root cmd systemctl args start ModemManager
|
||||
permit nopass :wheel as root cmd systemctl args stop ModemManager
|
||||
permit nopass :wheel as root cmd systemctl args restart ModemManager
|
||||
permit nopass :wheel as root cmd /usr/libexec/msm-modem-uim-selection
|
||||
permit nopass :wheel as root cmd msm-modem-uim-selection
|
||||
permit nopass :wheel as root cmd setup-timezone
|
||||
permit nopass :wheel as root cmd /sbin/setup-timezone
|
||||
permit nopass :wheel as root cmd rtcwake
|
||||
permit nopass :wheel as root cmd tee args -a /sys/power/wake_lock
|
||||
permit nopass :wheel as root cmd tee args -a /sys/power/wake_unlock
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Bananarchy Bluetooth menu wrapper — routes to native xnetworks Bluetooth tab
|
||||
if command -v xnetworks >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
|
||||
exec xnetworks bluetooth
|
||||
fi
|
||||
exec /usr/bin/dwm-plugin networks bluetooth
|
||||
@@ -98,7 +98,7 @@ checkforfinishedcalls() {
|
||||
|
||||
sxmo_notifs.sh new \
|
||||
-g "incoming-call-$FINISHEDNUMBER" \
|
||||
"TERMNAME='$NOTIFMSG' sxmo_terminal.sh sh -c \"echo '$NOTIFMSG at $(date)' && read\"" \
|
||||
"xcalllog" \
|
||||
"Missed $icon_phn $CONTACT ($FINISHEDNUMBER)"
|
||||
fi
|
||||
|
||||
@@ -280,7 +280,7 @@ checkfornewtexts() {
|
||||
if [ -z "$SXMO_DISABLE_SMS_NOTIFS" ]; then
|
||||
sxmo_notifs.sh new \
|
||||
-g "incoming-message-$NUM" \
|
||||
"sxmo_hook_tailtextlog.sh '$NUM'" \
|
||||
"xsms tailtextlog '$NUM'" \
|
||||
"$CONTACTNAME: $TEXT"
|
||||
|
||||
fi
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
if command -v xmodem >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
|
||||
exec xmodem info
|
||||
fi
|
||||
. sxmo_common.sh
|
||||
sxmo_terminal.sh sh -c "mmcli -m any && read"
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
if command -v xmodem >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
|
||||
exec xmodem log
|
||||
fi
|
||||
. sxmo_common.sh
|
||||
sxmo_terminal.sh sh -c "tail -n9999 -f $SXMO_LOGDIR/modemlog.tsv"
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Bananarchy Networks wrapper — routes to native xnetworks DWM plugin
|
||||
if command -v xnetworks >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
|
||||
exec xnetworks "$@"
|
||||
fi
|
||||
exec /usr/bin/dwm-plugin networks "$@"
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Bananarchy SIM PIN/PUK unlock wrapper — routes to native xmodem plugin UI
|
||||
|
||||
if command -v xmodem >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
|
||||
exec xmodem unlock
|
||||
fi
|
||||
|
||||
# Fallback if X11 plugin is not running
|
||||
. sxmo_common.sh
|
||||
SIM="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/([0-9]+)' | head -n1 | cut -d'/' -f2 || true)"
|
||||
[ -z "$SIM" ] && exit 0
|
||||
LOCKSTATE="$(mmcli -m any -K 2>/dev/null | grep 'modem.generic.unlock-required' | cut -d':' -f2 | tr -d ' ' || true)"
|
||||
case "$LOCKSTATE" in
|
||||
sim-pin)
|
||||
PIN="$(printf "0000\n1234\nCancel" | sxmo_dmenu.sh -p "PIN")"
|
||||
[ -z "$PIN" ] || [ "$PIN" = "Cancel" ] && exit 0
|
||||
mmcli -i "$SIM" --pin="$PIN"
|
||||
;;
|
||||
sim-puk)
|
||||
PUK="$(printf "Cancel\n" | sxmo_dmenu.sh -p "PUK (8 digits)")"
|
||||
[ -z "$PUK" ] || [ "$PUK" = "Cancel" ] && exit 0
|
||||
NEWPIN="$(printf "0000\n1234\nCancel" | sxmo_dmenu.sh -p "New PIN")"
|
||||
[ -z "$NEWPIN" ] || [ "$NEWPIN" = "Cancel" ] && exit 0
|
||||
mmcli -i "$SIM" --puk="$PUK" --pin="$NEWPIN"
|
||||
;;
|
||||
esac
|
||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+415
@@ -0,0 +1,415 @@
|
||||
#!/bin/sh
|
||||
# Bananarchy Dual-SIM & QMI UIM Provisioning Helper for Qualcomm MSM8905/MSM8909
|
||||
set -eu
|
||||
|
||||
CONF_FILE="/etc/conf.d/msm-modem-uim-selection"
|
||||
if [ -r "$CONF_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONF_FILE"
|
||||
fi
|
||||
|
||||
SIM_WAIT_TIME="${SIM_WAIT_TIME:-${sim_wait_time:-4}}"
|
||||
PREFERRED_SIM_SLOT="${PREFERRED_SIM_SLOT:-${preferred_sim_slot:-1}}"
|
||||
QMICLI_MODEM=""
|
||||
|
||||
detect_qmicli_modem() {
|
||||
if ! command -v qmicli >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
if [ -e "/dev/modem" ]; then
|
||||
QMICLI_MODEM="qmicli --silent -d /dev/modem"
|
||||
return 0
|
||||
elif [ -e "/dev/wwan0qmi0" ]; then
|
||||
QMICLI_MODEM="qmicli --silent -d /dev/wwan0qmi0 --device-open-qmi"
|
||||
return 0
|
||||
elif qmicli --silent -pd qrtr://0 --uim-noop >/dev/null 2>&1; then
|
||||
QMICLI_MODEM="qmicli --silent -pd qrtr://0"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_qmicli_modem() {
|
||||
max_wait="${1:-45}"
|
||||
count=0
|
||||
while [ "$count" -lt "$max_wait" ]; do
|
||||
if detect_qmicli_modem; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
count=$((count + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parse `qmicli --uim-get-card-status` into shell-safe KEY=VALUE lines
|
||||
parse_uim_card_status() {
|
||||
raw_status="$1"
|
||||
pref_slot="$2"
|
||||
printf "%s\n" "$raw_status" | awk -v pref="$pref_slot" '
|
||||
BEGIN {
|
||||
prim_slot = 0
|
||||
prim_app = 0
|
||||
cur_slot = 0
|
||||
in_app = 0
|
||||
for (s = 1; s <= 2; s++) {
|
||||
cstate[s] = "absent"
|
||||
rawstate[s] = "absent"
|
||||
upin_state[s] = "not-initialized"
|
||||
upin_ret[s] = 0
|
||||
upuk_ret[s] = 0
|
||||
app_type[s] = "none"
|
||||
app_state[s] = "none"
|
||||
aid[s] = ""
|
||||
pin1_state[s] = "not-initialized"
|
||||
pin1_ret[s] = 0
|
||||
puk1_ret[s] = 0
|
||||
}
|
||||
}
|
||||
/Primary GW:/ {
|
||||
if ($0 ~ /slot \x27[0-9]+\x27/) {
|
||||
line = $0
|
||||
sub(/.*slot \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
prim_slot = line + 0
|
||||
}
|
||||
if ($0 ~ /application \x27[0-9]+\x27/) {
|
||||
line = $0
|
||||
sub(/.*application \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
prim_app = line + 0
|
||||
}
|
||||
}
|
||||
/^[[:space:]]*Slot \[[0-9]+\]:/ {
|
||||
line = $0
|
||||
sub(/.*Slot \[/, "", line)
|
||||
sub(/\].*/, "", line)
|
||||
cur_slot = line + 0
|
||||
in_app = 0
|
||||
next
|
||||
}
|
||||
cur_slot >= 1 && cur_slot <= 2 {
|
||||
if ($0 ~ /^[[:space:]]*Application \[[0-9]+\]:/) {
|
||||
in_app = 1
|
||||
next
|
||||
}
|
||||
if (!in_app && $0 ~ /Card state:/) {
|
||||
line = $0
|
||||
sub(/.*Card state: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
rawstate[cur_slot] = line
|
||||
if (line == "present") cstate[cur_slot] = "present"
|
||||
else if (line == "absent" || line ~ /no-atr-received/) cstate[cur_slot] = "absent"
|
||||
else cstate[cur_slot] = "error"
|
||||
}
|
||||
if (!in_app && $0 ~ /UPIN state:/) {
|
||||
line = $0
|
||||
sub(/.*UPIN state: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
upin_state[cur_slot] = line
|
||||
}
|
||||
if (!in_app && $0 ~ /UPIN retries:/) {
|
||||
line = $0
|
||||
sub(/.*UPIN retries: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
upin_ret[cur_slot] = line + 0
|
||||
}
|
||||
if (!in_app && $0 ~ /UPUK retries:/) {
|
||||
line = $0
|
||||
sub(/.*UPUK retries: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
upuk_ret[cur_slot] = line + 0
|
||||
}
|
||||
if (in_app && $0 ~ /Application type:/) {
|
||||
line = $0
|
||||
sub(/.*Application type: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
if (line ~ /^usim/) {
|
||||
app_type[cur_slot] = "usim"
|
||||
want_aid[cur_slot] = 2
|
||||
} else if (line ~ /^sim/ && app_type[cur_slot] != "usim") {
|
||||
app_type[cur_slot] = "sim"
|
||||
want_aid[cur_slot] = 1
|
||||
} else {
|
||||
want_aid[cur_slot] = 0
|
||||
}
|
||||
}
|
||||
if (in_app && $0 ~ /Application state:/ && want_aid[cur_slot] > 0) {
|
||||
line = $0
|
||||
sub(/.*Application state: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
app_state[cur_slot] = line
|
||||
}
|
||||
if (in_app && $0 ~ /Application ID:/ && want_aid[cur_slot] > 0) {
|
||||
need_aid_line[cur_slot] = 1
|
||||
next
|
||||
}
|
||||
if (in_app && need_aid_line[cur_slot] == 1) {
|
||||
line = $0
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
||||
if (line != "") {
|
||||
aid[cur_slot] = line
|
||||
}
|
||||
need_aid_line[cur_slot] = 0
|
||||
}
|
||||
if (in_app && $0 ~ /PIN1 state:/ && want_aid[cur_slot] > 0) {
|
||||
line = $0
|
||||
sub(/.*PIN1 state: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
pin1_state[cur_slot] = line
|
||||
}
|
||||
if (in_app && $0 ~ /PIN1 retries:/ && want_aid[cur_slot] > 0) {
|
||||
line = $0
|
||||
sub(/.*PIN1 retries: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
pin1_ret[cur_slot] = line + 0
|
||||
}
|
||||
if (in_app && $0 ~ /PUK1 retries:/ && want_aid[cur_slot] > 0) {
|
||||
line = $0
|
||||
sub(/.*PUK1 retries: \x27/, "", line)
|
||||
sub(/\x27.*/, "", line)
|
||||
puk1_ret[cur_slot] = line + 0
|
||||
}
|
||||
}
|
||||
END {
|
||||
printf "PREFERRED_SLOT=%d\n", (pref == 2 ? 2 : 1)
|
||||
printf "PRIMARY_GW_SLOT=%d\n", prim_slot
|
||||
printf "PRIMARY_GW_APP=%d\n", prim_app
|
||||
for (s = 1; s <= 2; s++) {
|
||||
printf "SLOT%d_STATE=%s\n", s, cstate[s]
|
||||
printf "SLOT%d_RAW_STATE=%s\n", s, rawstate[s]
|
||||
printf "SLOT%d_APP_TYPE=%s\n", s, app_type[s]
|
||||
printf "SLOT%d_APP_STATE=%s\n", s, app_state[s]
|
||||
printf "SLOT%d_AID=%s\n", s, aid[s]
|
||||
printf "SLOT%d_PIN1_STATE=%s\n", s, pin1_state[s]
|
||||
printf "SLOT%d_PIN1_RETRIES=%d\n", s, pin1_ret[s]
|
||||
printf "SLOT%d_PUK1_RETRIES=%d\n", s, puk1_ret[s]
|
||||
printf "SLOT%d_UPIN_STATE=%s\n", s, upin_state[s]
|
||||
printf "SLOT%d_UPIN_RETRIES=%d\n", s, upin_ret[s]
|
||||
printf "SLOT%d_UPUK_RETRIES=%d\n", s, upuk_ret[s]
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
save_preferred_slot() {
|
||||
target_slot="$1"
|
||||
if [ -f "$CONF_FILE" ]; then
|
||||
if grep -q "^preferred_sim_slot=" "$CONF_FILE"; then
|
||||
sed -i "s/^preferred_sim_slot=.*/preferred_sim_slot=$target_slot/" "$CONF_FILE"
|
||||
else
|
||||
printf "\npreferred_sim_slot=%s\n" "$target_slot" >> "$CONF_FILE"
|
||||
fi
|
||||
else
|
||||
printf "sim_wait_time=4\npreferred_sim_slot=%s\n" "$target_slot" > "$CONF_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
provision_slot() {
|
||||
req_slot="$1"
|
||||
wait_secs="$2"
|
||||
|
||||
if ! wait_for_qmicli_modem "$wait_secs"; then
|
||||
echo "No modem available." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)"
|
||||
count=0
|
||||
while ! printf "%s" "$QMI_CARDS" | grep -Fq "Card state: 'present'"; do
|
||||
if [ "$count" -ge "$SIM_WAIT_TIME" ]; then
|
||||
echo "No SIM detected after $SIM_WAIT_TIME seconds."
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
count=$((count + 1))
|
||||
QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)"
|
||||
done
|
||||
|
||||
eval "$(parse_uim_card_status "$QMI_CARDS" "$req_slot")"
|
||||
|
||||
chosen_slot=""
|
||||
chosen_aid=""
|
||||
if [ "$req_slot" = "2" ]; then
|
||||
if [ "$SLOT2_STATE" = "present" ] && [ -n "$SLOT2_AID" ]; then
|
||||
chosen_slot="2"
|
||||
chosen_aid="$SLOT2_AID"
|
||||
elif [ "$SLOT1_STATE" = "present" ] && [ -n "$SLOT1_AID" ]; then
|
||||
chosen_slot="1"
|
||||
chosen_aid="$SLOT1_AID"
|
||||
fi
|
||||
else
|
||||
if [ "$SLOT1_STATE" = "present" ] && [ -n "$SLOT1_AID" ]; then
|
||||
chosen_slot="1"
|
||||
chosen_aid="$SLOT1_AID"
|
||||
elif [ "$SLOT2_STATE" = "present" ] && [ -n "$SLOT2_AID" ]; then
|
||||
chosen_slot="2"
|
||||
chosen_aid="$SLOT2_AID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$chosen_slot" ] || [ -z "$chosen_aid" ]; then
|
||||
echo "No valid USIM/SIM application found on any slot."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$PRIMARY_GW_SLOT" != "0" ]; then
|
||||
$QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Selecting AID $chosen_aid on slot $chosen_slot"
|
||||
$QMICLI_MODEM --uim-change-provisioning-session="slot=$chosen_slot,activate=yes,session-type=primary-gw-provisioning,aid=$chosen_aid" >/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
cmd="${1:-boot}"
|
||||
|
||||
case "$cmd" in
|
||||
boot)
|
||||
if command -v systemd-notify >/dev/null 2>&1; then
|
||||
trap '[ $? -eq 0 ] && systemd-notify --ready' EXIT
|
||||
fi
|
||||
case "$(cat /sys/devices/soc0/machine 2>/dev/null || true)" in
|
||||
APQ*)
|
||||
echo "Skipping SIM configuration on APQ SoC."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
provision_slot "$PREFERRED_SIM_SLOT" 45
|
||||
exit $?
|
||||
;;
|
||||
|
||||
status)
|
||||
if ! detect_qmicli_modem; then
|
||||
echo "MODEM_AVAILABLE=0"
|
||||
echo "PREFERRED_SLOT=$PREFERRED_SIM_SLOT"
|
||||
exit 0
|
||||
fi
|
||||
QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)"
|
||||
if [ -z "$QMI_CARDS" ]; then
|
||||
echo "MODEM_AVAILABLE=0"
|
||||
echo "PREFERRED_SLOT=$PREFERRED_SIM_SLOT"
|
||||
exit 0
|
||||
fi
|
||||
echo "MODEM_AVAILABLE=1"
|
||||
parse_uim_card_status "$QMI_CARDS" "$PREFERRED_SIM_SLOT"
|
||||
exit 0
|
||||
;;
|
||||
|
||||
switch)
|
||||
target="${2:-1}"
|
||||
if [ "$target" != "1" ] && [ "$target" != "2" ]; then
|
||||
echo "Invalid slot: $target (must be 1 or 2)" >&2
|
||||
exit 1
|
||||
fi
|
||||
save_preferred_slot "$target"
|
||||
SIM_WAIT_TIME=2
|
||||
provision_slot "$target" 5
|
||||
if command -v rc-service >/dev/null 2>&1; then
|
||||
rc-service modemmanager restart >/dev/null 2>&1 || true
|
||||
elif command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart ModemManager >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
|
||||
verify-pin)
|
||||
slot="${2:-1}"
|
||||
pin="${3:-}"
|
||||
if [ -z "$pin" ]; then
|
||||
echo "Missing PIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
sim_id="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)"
|
||||
if [ -n "$sim_id" ] && mmcli -i "$sim_id" --pin="$pin" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if detect_qmicli_modem; then
|
||||
if $QMICLI_MODEM --uim-verify-pin="PIN1,$pin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if $QMICLI_MODEM --uim-verify-pin="PIN1,$pin,session-type=card-slot-$slot" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
|
||||
unblock-puk)
|
||||
slot="${2:-1}"
|
||||
puk="${3:-}"
|
||||
newpin="${4:-}"
|
||||
if [ -z "$puk" ] || [ -z "$newpin" ]; then
|
||||
echo "Missing PUK or new PIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
sim_id="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)"
|
||||
if [ -n "$sim_id" ] && mmcli -i "$sim_id" --puk="$puk" --pin="$newpin" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if detect_qmicli_modem; then
|
||||
if $QMICLI_MODEM --uim-unblock-pin="PIN1,$puk,$newpin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if $QMICLI_MODEM --uim-unblock-pin="PIN1,$puk,$newpin,session-type=card-slot-$slot" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
|
||||
set-pin-lock)
|
||||
slot="${2:-1}"
|
||||
mode="${3:-enable}"
|
||||
pin="${4:-}"
|
||||
if [ -z "$pin" ] || { [ "$mode" != "enable" ] && [ "$mode" != "disable" ]; }; then
|
||||
echo "Usage: $0 set-pin-lock <1|2> <enable|disable> <pin>" >&2
|
||||
exit 1
|
||||
fi
|
||||
sim_id="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)"
|
||||
if [ -n "$sim_id" ]; then
|
||||
if [ "$mode" = "enable" ] && mmcli -i "$sim_id" --enable-pin --pin="$pin" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
elif [ "$mode" = "disable" ] && mmcli -i "$sim_id" --disable-pin --pin="$pin" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if detect_qmicli_modem; then
|
||||
if $QMICLI_MODEM --uim-set-pin-protection="PIN1,$mode,$pin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if $QMICLI_MODEM --uim-set-pin-protection="PIN1,$mode,$pin,session-type=card-slot-$slot" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
|
||||
change-pin)
|
||||
slot="${2:-1}"
|
||||
oldpin="${3:-}"
|
||||
newpin="${4:-}"
|
||||
if [ -z "$oldpin" ] || [ -z "$newpin" ]; then
|
||||
echo "Usage: $0 change-pin <1|2> <oldpin> <newpin>" >&2
|
||||
exit 1
|
||||
fi
|
||||
sim_id="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)"
|
||||
if [ -n "$sim_id" ] && mmcli -i "$sim_id" --change-pin="$newpin" --pin="$oldpin" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if detect_qmicli_modem; then
|
||||
if $QMICLI_MODEM --uim-change-pin="PIN1,$oldpin,$newpin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if $QMICLI_MODEM --uim-change-pin="PIN1,$oldpin,$newpin,session-type=card-slot-$slot" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 [boot|status|switch <1|2>|verify-pin <slot> <pin>|unblock-puk <slot> <puk> <newpin>|set-pin-lock <slot> <enable|disable> <pin>|change-pin <slot> <oldpin> <newpin>]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Regular → Executable
+9
@@ -9,6 +9,15 @@
|
||||
export PATH="${HOME:-/home/user}/.local/bin:${HOME:-/home/user}/.config/sxmo/hooks:/usr/share/sxmo/default_hooks:$PATH"
|
||||
. sxmo_hook_icons.sh
|
||||
|
||||
GAMES_CACHE="${XDG_RUNTIME_DIR:-/tmp}/sxmo_games.cache"
|
||||
if [ -f "$GAMES_CACHE" ] && [ ! "$0" -nt "$GAMES_CACHE" ] && [ ! /usr/bin -nt "$GAMES_CACHE" ] && [ ! /usr/local/bin -nt "$GAMES_CACHE" ] && [ ! "${HOME:-/home/user}/.local/bin" -nt "$GAMES_CACHE" ]; then
|
||||
cat "$GAMES_CACHE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec 3>&1 > "$GAMES_CACHE.tmp"
|
||||
trap 'mv -f "$GAMES_CACHE.tmp" "$GAMES_CACHE" 2>/dev/null; cat "$GAMES_CACHE" >&3 2>/dev/null' EXIT
|
||||
|
||||
write_line() {
|
||||
printf "%s ^ 0 ^ %s\n" "$1" "$2"
|
||||
}
|
||||
|
||||
@@ -16,38 +16,31 @@ newstate="$2"
|
||||
sxmo_debug "$2 (old: $1 [reason: $3])"
|
||||
|
||||
case "$newstate" in
|
||||
"locked")
|
||||
sxmo_log "State is locked. Attempting to unlock."
|
||||
if ! pgrep -f "sxmo_unlocksim.sh" >/dev/null; then
|
||||
sxmo_unlocksim.sh
|
||||
fi
|
||||
;;
|
||||
"enabling")
|
||||
sxmo_log "State is enabling. Clearing stale call files."
|
||||
rm "$XDG_RUNTIME_DIR"/sxmo_calls/* 2>/dev/null
|
||||
rm -f "$SXMO_NOTIFDIR"/incomingcall* 2>/dev/null
|
||||
;;
|
||||
"registered")
|
||||
sxmo_log "State is registered. Checking for calls and messages."
|
||||
"locked")
|
||||
sxmo_log "State is locked. Opening SIM PIN/PUK unlock dialog."
|
||||
if command -v xmodem >/dev/null 2>&1; then
|
||||
xmodem unlock >/dev/null 2>&1 &
|
||||
elif ! pgrep -f "sxmo_unlocksim.sh" >/dev/null; then
|
||||
sxmo_unlocksim.sh &
|
||||
fi
|
||||
;;
|
||||
"enabling")
|
||||
sxmo_log "State is enabling. Clearing stale call files."
|
||||
rm "$XDG_RUNTIME_DIR"/sxmo_calls/* 2>/dev/null || true
|
||||
rm -f "$SXMO_NOTIFDIR"/incomingcall* 2>/dev/null || true
|
||||
;;
|
||||
"registered")
|
||||
sxmo_log "State is registered. Checking for calls and messages."
|
||||
|
||||
# kill the pin-entry dmenu if still open
|
||||
pkill -f sxmo_unlocksim.sh
|
||||
# kill the pin-entry dmenu if still open
|
||||
pkill -f sxmo_unlocksim.sh 2>/dev/null || true
|
||||
|
||||
sxmo_modem.sh checkforfinishedcalls
|
||||
sxmo_modem.sh checkforincomingcalls
|
||||
sxmo_modem.sh checkfornewtexts
|
||||
sxmo_modem.sh checkforstucksms
|
||||
if [ -f "${SXMO_MMS_BASE_DIR:-"$HOME"/.mms/modemmanager}/mms" ]; then
|
||||
sxmo_mms.sh checkforlostmms
|
||||
fi
|
||||
|
||||
# An example of something else to do here!
|
||||
#if ! ping -Iwwan0 -q -c 1 -W 10 www.google.com >/dev/null; then
|
||||
# sxmo_log "ping failed."
|
||||
# nmcli c down "MINTY"
|
||||
# nmcli c up "MINTY"
|
||||
#else
|
||||
# sxmo_log "ping ok."
|
||||
#fi
|
||||
;;
|
||||
sxmo_modem.sh checkforfinishedcalls
|
||||
sxmo_modem.sh checkforincomingcalls
|
||||
sxmo_modem.sh checkfornewtexts
|
||||
sxmo_modem.sh checkforstucksms
|
||||
if [ -f "${SXMO_MMS_BASE_DIR:-"$HOME"/.mms/modemmanager}/mms" ]; then
|
||||
sxmo_mms.sh checkforlostmms
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -80,6 +80,9 @@ fi
|
||||
if [ -d "$BASE_DIR/prebuilt/sxmo-x11/usr" ]; then
|
||||
cp -af "$BASE_DIR/prebuilt/sxmo-x11/usr/." "$TARGET_ROOTFS/usr/"
|
||||
fi
|
||||
if [ -d "$BASE_DIR/prebuilt/games/usr" ]; then
|
||||
cp -af "$BASE_DIR/prebuilt/games/usr/." "$TARGET_ROOTFS/usr/"
|
||||
fi
|
||||
cp -af "$BASE_DIR/prebuilt/fcitx5-t9/usr/." "$TARGET_ROOTFS/usr/"
|
||||
cp -af "$BASE_DIR/prebuilt/bantam/bantam" "$TARGET_ROOTFS/usr/bin/bantam"
|
||||
cp -af "$BASE_DIR/prebuilt/bantam/bantam" "$TARGET_ROOTFS/usr/local/bin/bantam"
|
||||
|
||||
@@ -142,6 +142,14 @@ EOF
|
||||
/bananarchy/src/dwm-plugins/themes_plugin.cpp $PLUG_LIBS \
|
||||
-o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so
|
||||
|
||||
$CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \
|
||||
/bananarchy/src/dwm-plugins/modem_plugin.cpp $PLUG_LIBS \
|
||||
-o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so
|
||||
|
||||
$CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \
|
||||
/bananarchy/src/dwm-plugins/networks_plugin.cpp $PLUG_LIBS \
|
||||
-o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so
|
||||
|
||||
$STRIP --strip-unneeded \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so \
|
||||
@@ -151,7 +159,9 @@ EOF
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so \
|
||||
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so
|
||||
|
||||
$CC $OPT_CFLAGS -std=c99 -D_DEFAULT_SOURCE -Wall -Wextra \
|
||||
/bananarchy/src/dwm-plugins/dwm_plugin_runner.c -ldl \
|
||||
@@ -170,6 +180,9 @@ EOF
|
||||
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xfiles
|
||||
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcamera
|
||||
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xthemes
|
||||
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xmodem
|
||||
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xnetworks
|
||||
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xbluetooth
|
||||
|
||||
mkdir -p /bananarchy/overlay/rootfs/usr/bin /bananarchy/overlay/rootfs/usr/lib/dwm/plugins
|
||||
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/
|
||||
|
||||
@@ -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