diff --git a/AGENTS.md b/AGENTS.md index e76d824..2d653dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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_.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. diff --git a/MANUAL.md b/MANUAL.md index 8221188..0d887d0 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -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 --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_.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 --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_.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 --preload` with an exclusive `/tmp/dwm_plugin_.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_.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 --preload` with an exclusive `/tmp/dwm_plugin_.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_.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 ` 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]`). diff --git a/README.md b/README.md index 3fcc8f0..1880757 100644 --- a/README.md +++ b/README.md @@ -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__.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//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 diff --git a/overlay/rootfs/etc/conf.d/msm-modem-uim-selection b/overlay/rootfs/etc/conf.d/msm-modem-uim-selection index 6d257d9..fe3d33d 100644 --- a/overlay/rootfs/etc/conf.d/msm-modem-uim-selection +++ b/overlay/rootfs/etc/conf.d/msm-modem-uim-selection @@ -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 diff --git a/overlay/rootfs/etc/doas.d/50-sxmo.conf b/overlay/rootfs/etc/doas.d/50-sxmo.conf index 0518929..417ce70 100644 --- a/overlay/rootfs/etc/doas.d/50-sxmo.conf +++ b/overlay/rootfs/etc/doas.d/50-sxmo.conf @@ -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 diff --git a/overlay/rootfs/usr/bin/dwm b/overlay/rootfs/usr/bin/dwm index 035bcbb..d5fb326 100755 Binary files a/overlay/rootfs/usr/bin/dwm and b/overlay/rootfs/usr/bin/dwm differ diff --git a/overlay/rootfs/usr/bin/dwm-plugin b/overlay/rootfs/usr/bin/dwm-plugin index 9e3f0a3..a1aca55 100755 Binary files a/overlay/rootfs/usr/bin/dwm-plugin and b/overlay/rootfs/usr/bin/dwm-plugin differ diff --git a/overlay/rootfs/usr/bin/snake b/overlay/rootfs/usr/bin/snake index 8bdf63a..696b6e0 100755 Binary files a/overlay/rootfs/usr/bin/snake and b/overlay/rootfs/usr/bin/snake differ diff --git a/overlay/rootfs/usr/bin/sxmo_bluetoothmenu.sh b/overlay/rootfs/usr/bin/sxmo_bluetoothmenu.sh new file mode 100755 index 0000000..cb964ba --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_bluetoothmenu.sh @@ -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 diff --git a/overlay/rootfs/usr/bin/sxmo_modem.sh b/overlay/rootfs/usr/bin/sxmo_modem.sh index 4aa6025..d641fb8 100755 --- a/overlay/rootfs/usr/bin/sxmo_modem.sh +++ b/overlay/rootfs/usr/bin/sxmo_modem.sh @@ -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 diff --git a/overlay/rootfs/usr/bin/sxmo_modeminfo.sh b/overlay/rootfs/usr/bin/sxmo_modeminfo.sh new file mode 100755 index 0000000..7528724 --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_modeminfo.sh @@ -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" diff --git a/overlay/rootfs/usr/bin/sxmo_modemlog.sh b/overlay/rootfs/usr/bin/sxmo_modemlog.sh new file mode 100755 index 0000000..5823338 --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_modemlog.sh @@ -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" diff --git a/overlay/rootfs/usr/bin/sxmo_networks.sh b/overlay/rootfs/usr/bin/sxmo_networks.sh new file mode 100755 index 0000000..a46a704 --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_networks.sh @@ -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 "$@" diff --git a/overlay/rootfs/usr/bin/sxmo_unlocksim.sh b/overlay/rootfs/usr/bin/sxmo_unlocksim.sh new file mode 100755 index 0000000..140aab2 --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_unlocksim.sh @@ -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 diff --git a/overlay/rootfs/usr/bin/tetris b/overlay/rootfs/usr/bin/tetris index ebc99c4..249c4a7 100755 Binary files a/overlay/rootfs/usr/bin/tetris and b/overlay/rootfs/usr/bin/tetris differ diff --git a/overlay/rootfs/usr/bin/xbluetooth b/overlay/rootfs/usr/bin/xbluetooth new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xbluetooth @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/xmodem b/overlay/rootfs/usr/bin/xmodem new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xmodem @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/xnetworks b/overlay/rootfs/usr/bin/xnetworks new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xnetworks @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so index 44d9f26..8a7e0e0 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so and b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so index 54db343..c21e833 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so and b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/camera.so b/overlay/rootfs/usr/lib/dwm/plugins/camera.so index a8c5b21..382407d 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/camera.so and b/overlay/rootfs/usr/lib/dwm/plugins/camera.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so index 9f5f8ef..a0019e1 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so and b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/dialer.so b/overlay/rootfs/usr/lib/dwm/plugins/dialer.so index 67c4b6f..a3e1464 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/dialer.so and b/overlay/rootfs/usr/lib/dwm/plugins/dialer.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so b/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so index 32cb263..716adb7 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so and b/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/files.so b/overlay/rootfs/usr/lib/dwm/plugins/files.so index daf5a67..73b7771 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/files.so and b/overlay/rootfs/usr/lib/dwm/plugins/files.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/modem.so b/overlay/rootfs/usr/lib/dwm/plugins/modem.so new file mode 100755 index 0000000..157ff30 Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/modem.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/networks.so b/overlay/rootfs/usr/lib/dwm/plugins/networks.so new file mode 100755 index 0000000..811d380 Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/networks.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/sms.so b/overlay/rootfs/usr/lib/dwm/plugins/sms.so index f12b0e2..5d4e95e 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/sms.so and b/overlay/rootfs/usr/lib/dwm/plugins/sms.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/themes.so b/overlay/rootfs/usr/lib/dwm/plugins/themes.so index 73d0912..0adc50e 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/themes.so and b/overlay/rootfs/usr/lib/dwm/plugins/themes.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so index d4eac0f..f4a4a0f 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so and b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/overlay/rootfs/usr/libexec/msm-modem-uim-selection b/overlay/rootfs/usr/libexec/msm-modem-uim-selection new file mode 100755 index 0000000..6cca8dc --- /dev/null +++ b/overlay/rootfs/usr/libexec/msm-modem-uim-selection @@ -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> " >&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> " >&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 |unblock-puk |set-pin-lock |change-pin ]" >&2 + exit 1 + ;; +esac diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh old mode 100644 new mode 100755 index 49335cc..d61c744 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_games.sh @@ -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" } diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_modem.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_modem.sh index ffa6804..03c96d8 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_modem.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_modem.sh @@ -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 diff --git a/prebuilt/games/usr/bin/snake b/prebuilt/games/usr/bin/snake index 8bdf63a..696b6e0 100755 Binary files a/prebuilt/games/usr/bin/snake and b/prebuilt/games/usr/bin/snake differ diff --git a/prebuilt/games/usr/bin/tetris b/prebuilt/games/usr/bin/tetris index ebc99c4..249c4a7 100755 Binary files a/prebuilt/games/usr/bin/tetris and b/prebuilt/games/usr/bin/tetris differ diff --git a/prebuilt/sxmo-x11/usr/bin/dwm b/prebuilt/sxmo-x11/usr/bin/dwm index 035bcbb..d5fb326 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dwm and b/prebuilt/sxmo-x11/usr/bin/dwm differ diff --git a/prebuilt/sxmo-x11/usr/bin/dwm-plugin b/prebuilt/sxmo-x11/usr/bin/dwm-plugin index 9e3f0a3..a1aca55 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dwm-plugin and b/prebuilt/sxmo-x11/usr/bin/dwm-plugin differ diff --git a/prebuilt/sxmo-x11/usr/bin/xbluetooth b/prebuilt/sxmo-x11/usr/bin/xbluetooth new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xbluetooth @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/bin/xmodem b/prebuilt/sxmo-x11/usr/bin/xmodem new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xmodem @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/bin/xnetworks b/prebuilt/sxmo-x11/usr/bin/xnetworks new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xnetworks @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so index 44d9f26..8a7e0e0 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so index 54db343..c21e833 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so index a8c5b21..382407d 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so index 9f5f8ef..a0019e1 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so index 67c4b6f..a3e1464 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so index 32cb263..716adb7 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so index daf5a67..73b7771 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so new file mode 100755 index 0000000..157ff30 Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so new file mode 100755 index 0000000..811d380 Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so index f12b0e2..5d4e95e 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so index 73d0912..0adc50e 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so index d4eac0f..f4a4a0f 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/scripts/apply-bananarchy-overlay.sh b/scripts/apply-bananarchy-overlay.sh index ef17444..f1d3d81 100755 --- a/scripts/apply-bananarchy-overlay.sh +++ b/scripts/apply-bananarchy-overlay.sh @@ -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" diff --git a/scripts/build-sxmo-x11.sh b/scripts/build-sxmo-x11.sh index 314372e..fd75b27 100755 --- a/scripts/build-sxmo-x11.sh +++ b/scripts/build-sxmo-x11.sh @@ -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/ diff --git a/src/dwm-plugins/dwm_plugin_runner.c b/src/dwm-plugins/dwm_plugin_runner.c index d4b616e..435362b 100644 --- a/src/dwm-plugins/dwm_plugin_runner.c +++ b/src/dwm-plugins/dwm_plugin_runner.c @@ -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; } diff --git a/src/dwm-plugins/modem_plugin.cpp b/src/dwm-plugins/modem_plugin.cpp new file mode 100644 index 0000000..a8d9944 --- /dev/null +++ b/src/dwm-plugins/modem_plugin.cpp @@ -0,0 +1,1638 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Bananarchy Modem, Dual-SIM & PIN/PUK Manager Plugin (modem.so / xmodem) + +#include "phone_data.h" +#include "../sxmo-dwm/dwm_plugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef None +#undef None +#endif + +namespace { + +using namespace bananarchy; + +volatile sig_atomic_t gRunning = 1; + +void handleSignal(int) { + gRunning = 0; +} + +enum class TabId { + SimCards = 0, + RadioInfo = 1, + ModemLog = 2, +}; + +enum class ModalMode { + None = 0, + SimActions = 1, + VerifyPin = 2, + UnblockPuk = 3, + TogglePinLock = 4, + ChangePin = 5, + UssdDialog = 6, +}; + +struct SimSlotInfo { + int slotNum = 1; + std::string state = "absent"; // present, absent, error + std::string rawState = "absent"; + std::string appType = "none"; // usim, sim, none + std::string appState = "none"; + std::string aid; + std::string pin1State = "not-initialized"; + int pin1Retries = 0; + int puk1Retries = 0; + std::string upinState = "not-initialized"; + int upinRetries = 0; + int upukRetries = 0; + + bool isActiveProvisioned = false; + bool isPreferred = false; + + bool needsPin() const { + return state == "present" && + (pin1State == "enabled-not-verified" || appState == "pin1"); + } + + bool needsPuk() const { + return state == "present" && + (pin1State == "blocked" || appState == "puk1"); + } + + bool isPinEnabled() const { + return pin1State == "enabled-not-verified" || + pin1State == "enabled-verified" || + pin1State == "blocked"; + } +}; + +struct ModemStatusData { + bool qmiAvailable = false; + int preferredSlot = 1; + int primaryGwSlot = 0; + SimSlotInfo slots[2]; + + bool mmPresent = false; + std::string mmState = "nomodem"; + std::string mmFailedReason; + std::string mmPowerState = "off"; + std::string mmUnlockRequired; + int mmPinRetries = -1; + int mmPukRetries = -1; + std::string operatorName; + std::string operatorCode; + std::string accessTech; + int signalQuality = 0; + std::string imei; + std::string ownNumber; + std::string simDbusPath; + std::string simIccid; + std::string simOperatorName; + std::string currentModes; + int modePresetIdx = 0; +}; + +struct RadioModePreset { + const char* label; + const char* allowed; + const char* preferred; +}; + +constexpr RadioModePreset kRadioModes[] = { + {"2G/3G/4G (4G Pref)", "2g|3g|4g", "4g"}, + {"2G/3G/4G (3G Pref)", "2g|3g|4g", "3g"}, + {"2G/3G (3G Pref)", "2g|3g", "3g"}, + {"4G LTE Only", "4g", "none"}, + {"3G UMTS Only", "3g", "none"}, + {"2G GSM Only", "2g", "none"}, +}; +constexpr int kNumRadioModes = + static_cast(sizeof(kRadioModes) / sizeof(kRadioModes[0])); + +struct ModemLogRow { + std::string timeStr; + std::string eventType; + std::string target; + std::string detail; +}; + +std::string shellQuote(const std::string& s) { + std::string out = "'"; + for (char c : s) { + if (c == '\'') { + out += "'\\''"; + } else { + out.push_back(c); + } + } + out.push_back('\''); + return out; +} + +void parseKvLines(const std::string& text, + std::unordered_map& out, + char sep) { + std::istringstream ss(text); + std::string line; + while (std::getline(ss, line)) { + size_t pos = line.find(sep); + if (pos == std::string::npos) continue; + std::string k = trimStr(line.substr(0, pos)); + std::string v = trimStr(line.substr(pos + 1)); + if (!k.empty()) { + out[k] = v; + } + } +} + +ModemStatusData queryModemAndSimStatus() { + ModemStatusData d; + d.slots[0].slotNum = 1; + d.slots[1].slotNum = 2; + + // 1. Query QMI UIM status for both physical slots via helper + std::string uimOut = runCaptureCmd( + "doas -n /usr/libexec/msm-modem-uim-selection status 2>/dev/null || " + "/usr/libexec/msm-modem-uim-selection status 2>/dev/null"); + std::unordered_map uimKv; + parseKvLines(uimOut, uimKv, '='); + + d.qmiAvailable = (uimKv["MODEM_AVAILABLE"] == "1"); + if (!uimKv["PREFERRED_SLOT"].empty()) { + d.preferredSlot = std::clamp(std::atoi(uimKv["PREFERRED_SLOT"].c_str()), 1, 2); + } + if (!uimKv["PRIMARY_GW_SLOT"].empty()) { + d.primaryGwSlot = std::atoi(uimKv["PRIMARY_GW_SLOT"].c_str()); + } + + for (int i = 0; i < 2; ++i) { + std::string pfx = "SLOT" + std::to_string(i + 1) + "_"; + auto& s = d.slots[i]; + s.slotNum = i + 1; + if (!uimKv[pfx + "STATE"].empty()) s.state = uimKv[pfx + "STATE"]; + if (!uimKv[pfx + "RAW_STATE"].empty()) s.rawState = uimKv[pfx + "RAW_STATE"]; + if (!uimKv[pfx + "APP_TYPE"].empty()) s.appType = uimKv[pfx + "APP_TYPE"]; + if (!uimKv[pfx + "APP_STATE"].empty()) s.appState = uimKv[pfx + "APP_STATE"]; + s.aid = uimKv[pfx + "AID"]; + if (!uimKv[pfx + "PIN1_STATE"].empty()) s.pin1State = uimKv[pfx + "PIN1_STATE"]; + if (!uimKv[pfx + "PIN1_RETRIES"].empty()) { + s.pin1Retries = std::atoi(uimKv[pfx + "PIN1_RETRIES"].c_str()); + } + if (!uimKv[pfx + "PUK1_RETRIES"].empty()) { + s.puk1Retries = std::atoi(uimKv[pfx + "PUK1_RETRIES"].c_str()); + } + if (!uimKv[pfx + "UPIN_STATE"].empty()) s.upinState = uimKv[pfx + "UPIN_STATE"]; + if (!uimKv[pfx + "UPIN_RETRIES"].empty()) { + s.upinRetries = std::atoi(uimKv[pfx + "UPIN_RETRIES"].c_str()); + } + if (!uimKv[pfx + "UPUK_RETRIES"].empty()) { + s.upukRetries = std::atoi(uimKv[pfx + "UPUK_RETRIES"].c_str()); + } + s.isActiveProvisioned = (d.primaryGwSlot == (i + 1)); + s.isPreferred = (d.preferredSlot == (i + 1)); + } + + // 2. Query ModemManager (-m any -K) + std::string mmOut = runCaptureCmd("mmcli -m any -K 2>/dev/null"); + std::unordered_map mmKv; + parseKvLines(mmOut, mmKv, ':'); + + auto cleanMmVal = [](const std::string& v) -> std::string { + return (v == "--") ? "" : v; + }; + + if (!mmKv["modem.dbus-path"].empty()) { + d.mmPresent = true; + d.mmState = cleanMmVal(mmKv["modem.generic.state"]); + d.mmFailedReason = cleanMmVal(mmKv["modem.generic.state-failed-reason"]); + d.mmPowerState = cleanMmVal(mmKv["modem.generic.power-state"]); + d.mmUnlockRequired = cleanMmVal(mmKv["modem.generic.unlock-required"]); + d.operatorName = cleanMmVal(mmKv["modem.3gpp.operator-name"]); + d.operatorCode = cleanMmVal(mmKv["modem.3gpp.operator-code"]); + d.accessTech = cleanMmVal(mmKv["modem.generic.access-technologies.value[1]"]); + if (d.accessTech.empty()) { + d.accessTech = cleanMmVal(mmKv["modem.generic.access-technologies"]); + } + std::string sigStr = cleanMmVal(mmKv["modem.generic.signal-quality.value"]); + if (!sigStr.empty()) { + d.signalQuality = std::clamp(std::atoi(sigStr.c_str()), 0, 100); + } + d.imei = cleanMmVal(mmKv["modem.3gpp.imei"]); + if (d.imei.empty()) { + d.imei = cleanMmVal(mmKv["modem.generic.equipment-identifier"]); + } + d.ownNumber = cleanMmVal(mmKv["modem.generic.own-numbers.value[1]"]); + d.simDbusPath = cleanMmVal(mmKv["modem.generic.sim"]); + d.currentModes = cleanMmVal(mmKv["modem.generic.current-modes"]); + + if (d.currentModes.find("allowed: 2g, 3g, 4g; preferred: 4g") != std::string::npos) { + d.modePresetIdx = 0; + } else if (d.currentModes.find("allowed: 2g, 3g, 4g; preferred: 3g") != std::string::npos) { + d.modePresetIdx = 1; + } else if (d.currentModes.find("allowed: 2g, 3g; preferred: 3g") != std::string::npos) { + d.modePresetIdx = 2; + } else if (d.currentModes.find("allowed: 4g") != std::string::npos) { + d.modePresetIdx = 3; + } else if (d.currentModes.find("allowed: 3g") != std::string::npos) { + d.modePresetIdx = 4; + } else if (d.currentModes.find("allowed: 2g") != std::string::npos) { + d.modePresetIdx = 5; + } + + // Parse unlock-retries if present, e.g. "sim-pin (3), sim-puk (10)" + for (int idx = 1; idx <= 6; ++idx) { + std::string rk = "modem.generic.unlock-retries.value[" + std::to_string(idx) + "]"; + std::string rv = cleanMmVal(mmKv[rk]); + if (rv.empty()) break; + if (rv.find("sim-pin (") != std::string::npos) { + size_t p = rv.find('('); + if (p != std::string::npos) d.mmPinRetries = std::atoi(rv.c_str() + p + 1); + } else if (rv.find("sim-puk (") != std::string::npos) { + size_t p = rv.find('('); + if (p != std::string::npos) d.mmPukRetries = std::atoi(rv.c_str() + p + 1); + } + } + } + + // 3. Query active SIM object if available + if (!d.simDbusPath.empty()) { + std::string simOut = runCaptureCmd( + "mmcli -i " + shellQuote(d.simDbusPath) + " -K 2>/dev/null"); + std::unordered_map simKv; + parseKvLines(simOut, simKv, ':'); + d.simIccid = cleanMmVal(simKv["sim.properties.iccid"]); + d.simOperatorName = cleanMmVal(simKv["sim.properties.operator-name"]); + if (d.operatorName.empty() && !d.simOperatorName.empty()) { + d.operatorName = d.simOperatorName; + } + } + + // Sync ModemManager lock info into active slot if QMI didn't report retries + int actIdx = (d.primaryGwSlot >= 1 && d.primaryGwSlot <= 2) + ? (d.primaryGwSlot - 1) + : (d.preferredSlot - 1); + if (actIdx >= 0 && actIdx < 2) { + if (d.mmUnlockRequired == "sim-pin" && d.slots[actIdx].pin1State == "not-initialized") { + d.slots[actIdx].state = "present"; + d.slots[actIdx].pin1State = "enabled-not-verified"; + } else if (d.mmUnlockRequired == "sim-puk") { + d.slots[actIdx].state = "present"; + d.slots[actIdx].pin1State = "blocked"; + } + if (d.mmPinRetries >= 0 && d.slots[actIdx].pin1Retries == 0) { + d.slots[actIdx].pin1Retries = d.mmPinRetries; + } + if (d.mmPukRetries >= 0 && d.slots[actIdx].puk1Retries == 0) { + d.slots[actIdx].puk1Retries = d.mmPukRetries; + } + } + + return d; +} + +std::vector loadRecentModemLog(const ContactBook& book, + size_t maxEntries = 80) { + std::vector out; + std::ifstream in(getModemLogFilePath()); + if (!in.is_open()) return out; + + std::vector lines; + std::string line; + while (std::getline(in, line)) { + if (!line.empty()) lines.push_back(line); + } + + for (auto it = lines.rbegin(); + it != lines.rend() && out.size() < maxEntries; ++it) { + size_t t1 = it->find('\t'); + if (t1 == std::string::npos) continue; + size_t t2 = it->find('\t', t1 + 1); + if (t2 == std::string::npos) continue; + size_t t3 = it->find('\t', t2 + 1); + + ModemLogRow row; + row.timeStr = formatShortTime(trimStr(it->substr(0, t1))); + row.eventType = trimStr(it->substr(t1 + 1, t2 - t1 - 1)); + std::string rawNum = trimStr( + t3 == std::string::npos ? it->substr(t2 + 1) + : it->substr(t2 + 1, t3 - t2 - 1)); + std::string cname = book.resolveName(rawNum); + row.target = cname.empty() ? rawNum : (cname + " (" + rawNum + ")"); + if (t3 != std::string::npos) { + row.detail = trimStr(it->substr(t3 + 1)); + } + out.push_back(row); + } + return out; +} + +int runModemApp(int argc, char** argv) { + gRunning = 1; + std::setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + + bool isPreload = false; + std::string openArg; + for (int i = 1; i < argc; ++i) { + if (!argv[i]) continue; + if (std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } else if (argv[i][0] != '-' && openArg.empty()) { + openArg = argv[i]; + } + } + + std::string daemonPidPath = getPluginDaemonPidPath("modem"); + std::string activePidPath = getRuntimeFile("xmodem.pid"); + + if (isPluginDaemonAlive("modem")) { + if (!isPreload) { + sendPluginCommand("modem", + openArg.empty() ? "OPEN" : ("OPEN " + openArg)); + } + return 0; + } + + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("modem"); + + struct sigaction sa {}; + sa.sa_handler = handleSignal; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) { + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + return 1; + } + + int screen = DefaultScreen(dpy); + Window root = RootWindow(dpy, screen); + Visual* visual = DefaultVisual(dpy, screen); + Colormap cmap = DefaultColormap(dpy, screen); + + ThemeSpec initSpec; + initSpec.loadActive(dpy, screen); + + XSetWindowAttributes swa {}; + swa.override_redirect = True; + swa.background_pixel = initSpec.bgMain; + swa.border_pixel = initSpec.selBg; + swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | + VisibilityChangeMask | StructureNotifyMask; + + Window win = XCreateWindow( + dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, + DefaultDepth(dpy, screen), CopyFromParent, visual, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); + + XClassHint ch {}; + char resName[] = "xmodem"; + char resClass[] = "XModem"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Modem & SIM"); + + Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, + DefaultDepth(dpy, screen)); + GC gc = XCreateGC(dpy, pix, 0, nullptr); + XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); + + PluginTheme pt; + pt.init(dpy, screen, visual, cmap, win); + auto& layoutMain = pt.layoutMain; + auto& layoutSmall = pt.layoutSmall; + auto& layoutIcon = pt.layoutIcon; + auto& colWhite = pt.colWhite; + auto& colLight = pt.colLight; + auto& colMuted = pt.colMuted; + auto& colYellow = pt.colYellow; + auto& colGreen = pt.colGreen; + auto& colBlue = pt.colBlue; + auto& colOrange = pt.colOrange; + auto& colRed = pt.colRed; + + ContactBook book; + ModemStatusData status; + std::vector logRows; + + TabId activeTab = TabId::SimCards; + ModalMode modal = ModalMode::None; + + int simRowSel = 0; // 0 = SIM 1, 1 = SIM 2, 2 = Switch Active, 3 = Unlock PIN/PUK, 4 = PIN Lock Toggle, 5 = Change PIN + int radioRowSel = 0; // 0 = Network Mode, 1 = USSD, 2 = Modem Power, 3 = Restart MM, 4 = Refresh + int logRowSel = 0; + int actionSel = 0; + int targetSlot = 1; + + // PIN / PUK / Change PIN wizard state + int wizardStep = 0; + bool revealDigits = false; + std::string inputPin1; + std::string inputPin2; + std::string inputPin3; + std::string modalError; + + // USSD state + std::string ussdInput = "*100#"; + std::string ussdResponse; + bool ussdWaitingReply = false; + + std::string statusBanner; + int64_t statusBannerUntilMs = 0; + + bool visible = false; + int64_t openStartMs = 0; + int64_t startupIgnoreUntilMs = 0; + int64_t lastRefreshMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + auto setBanner = [&](const std::string& msg, int durationMs = 2400) { + statusBanner = msg; + statusBannerUntilMs = nowMs() + durationMs; + }; + + auto refreshData = [&](bool reloadLog = false) { + status = queryModemAndSimStatus(); + if (reloadLog || activeTab == TabId::ModemLog) { + book.load(); + logRows = loadRecentModemLog(book); + if (logRowSel >= static_cast(logRows.size())) { + logRowSel = std::max(0, static_cast(logRows.size()) - 1); + } + } + lastRefreshMs = nowMs(); + }; + + auto openPinOrPukModalForSlot = [&](int slotNum) { + targetSlot = std::clamp(slotNum, 1, 2); + const auto& s = status.slots[targetSlot - 1]; + wizardStep = 0; + revealDigits = false; + inputPin1.clear(); + inputPin2.clear(); + inputPin3.clear(); + modalError.clear(); + if (s.needsPuk() || status.mmUnlockRequired == "sim-puk") { + modal = ModalMode::UnblockPuk; + } else { + modal = ModalMode::VerifyPin; + } + }; + + auto applyOpenArg = [&](const std::string& arg) { + std::string a = toLowerStr(trimStr(arg)); + modal = ModalMode::None; + if (a == "unlock" || a == "pin" || a == "puk") { + activeTab = TabId::SimCards; + int lockSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) + ? status.primaryGwSlot + : status.preferredSlot; + if (status.slots[0].needsPin() || status.slots[0].needsPuk()) { + lockSlot = 1; + } else if (status.slots[1].needsPin() || status.slots[1].needsPuk()) { + lockSlot = 2; + } + simRowSel = lockSlot - 1; + openPinOrPukModalForSlot(lockSlot); + } else if (a == "info" || a == "status" || a == "radio") { + activeTab = TabId::RadioInfo; + } else if (a == "log" || a == "logs") { + activeTab = TabId::ModemLog; + book.load(); + logRows = loadRecentModemLog(book); + } else if (a == "ussd") { + activeTab = TabId::RadioInfo; + modal = ModalMode::UssdDialog; + ussdResponse.clear(); + ussdWaitingReply = false; + } else { + activeTab = TabId::SimCards; + // If active SIM is locked, auto-highlight it + if (status.slots[0].needsPin() || status.slots[0].needsPuk()) { + simRowSel = 0; + } else if (status.slots[1].needsPin() || status.slots[1].needsPuk()) { + simRowSel = 1; + } + } + }; + + auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, + const std::string& text, int maxW = -1) -> int { + pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); + if (maxW > 0) { + pango_layout_set_width(layout, maxW * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } else { + pango_layout_set_width(layout, -1); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE); + } + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + int dy = y + std::max(0, (h - th) / 2); + pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, + dy * PANGO_SCALE); + return tw; + }; + + auto pangoTextWidth = [&](PangoLayout* layout, const std::string& text) -> int { + pango_layout_set_width(layout, -1); + pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + return tw; + }; + + auto drawSoftkeyBar = [&](const std::string& leftLabel, + const std::string& centerLabel, + const std::string& rightLabel) { + const int barY = kWinHeight - 26; + XSetForeground(dpy, gc, pt.spec.bgFooter); + XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26); + XSetForeground(dpy, gc, pt.spec.divider); + XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); + + drawPango(layoutMain, &colBlue, 6, barY, 26, leftLabel); + int cw = pangoTextWidth(layoutMain, centerLabel); + drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26, centerLabel); + int rw = pangoTextWidth(layoutMain, rightLabel); + drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel); + }; + + auto drawSignalBars = [&](int x, int y, int pct) { + int bars = (pct <= 0) ? 0 : (pct < 25 ? 1 : (pct < 50 ? 2 : (pct < 75 ? 3 : 4))); + for (int i = 0; i < 4; ++i) { + int bh = 4 + i * 3; + int bx = x + i * 5; + int by = y + (14 - bh); + XSetForeground(dpy, gc, (i < bars) ? pt.spec.success : pt.spec.border); + XFillRectangle(dpy, pix, gc, bx, by, 3, bh); + } + }; + + auto maskPinStr = [&](const std::string& s) -> std::string { + if (revealDigits) return s; + return std::string(s.size(), '*'); + }; + + auto render = [&]() { + if (!visible) return; + pt.checkAndReload(); + + XSetForeground(dpy, gc, pt.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // Top Tab Bar (Y=0..30) + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 30); + XSetForeground(dpy, gc, pt.spec.border); + XDrawLine(dpy, pix, gc, 0, 29, kWinWidth, 29); + + const char* tabLabels[3] = {"1:SIMs", "2:Radio", "3:Log"}; + int tabW = kWinWidth / 3; + for (int t = 0; t < 3; ++t) { + bool isSel = (static_cast(activeTab) == t); + if (isSel) { + XSetForeground(dpy, gc, pt.spec.selBg); + XFillRectangle(dpy, pix, gc, t * tabW, 0, tabW, 29); + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, t * tabW, 27, tabW, 2); + } + int tw = pangoTextWidth(layoutSmall, tabLabels[t]); + drawPango(layoutSmall, isSel ? &colWhite : &colMuted, + t * tabW + (tabW - tw) / 2, 0, 28, tabLabels[t]); + } + + const int contentY = 32; + const int contentH = kWinHeight - 32 - 26; + + if (activeTab == TabId::SimCards) { + // Draw SIM 1 & SIM 2 Cards (height 68px each) + for (int i = 0; i < 2; ++i) { + const auto& s = status.slots[i]; + int cardY = contentY + i * 70; + bool rowSelected = (simRowSel == i); + + XSetForeground(dpy, gc, rowSelected ? pt.spec.selBg : pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 8, 66); + XSetForeground(dpy, gc, rowSelected ? pt.spec.accent : pt.spec.border); + XDrawRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 9, 65); + + drawPango(layoutIcon, s.state == "present" ? &colGreen : &colMuted, + 10, cardY + 2, 22, kIconSim); + + std::string title = "SIM Slot " + std::to_string(s.slotNum); + if (s.isActiveProvisioned) { + title += " [ACTIVE]"; + } else if (s.isPreferred) { + title += " [PREF]"; + } + drawPango(layoutMain, rowSelected ? &colWhite : &colLight, + 30, cardY + 2, 22, title, 198); + + std::string line2; + XftColor* line2Col = &colMuted; + if (s.state == "present") { + if (s.needsPuk()) { + line2 = "BLOCKED (Enter PUK)"; + line2Col = &colRed; + } else if (s.needsPin()) { + line2 = "LOCKED (Enter PIN)"; + line2Col = &colOrange; + } else if (s.isActiveProvisioned) { + std::string op = status.operatorName.empty() ? "Ready" : status.operatorName; + if (!status.ownNumber.empty()) op += " " + status.ownNumber; + line2 = op; + line2Col = &colGreen; + } else { + line2 = "Present (Standby " + s.appType + ")"; + line2Col = &colBlue; + } + } else if (s.state == "error") { + line2 = "No SIM (No ATR)"; + } else { + line2 = "Empty Slot"; + } + drawPango(layoutSmall, line2Col, 10, cardY + 24, 20, line2, 220); + + std::string line3; + if (s.state == "present") { + std::string pState = s.pin1State; + if (pState == "enabled-not-verified") pState = "PIN Req"; + else if (pState == "enabled-verified") pState = "PIN On"; + else if (pState == "disabled") pState = "PIN Off"; + else if (pState == "blocked") pState = "PUK Req"; + line3 = pState + " (PIN:" + std::to_string(s.pin1Retries) + + " PUK:" + std::to_string(s.puk1Retries) + ")"; + } else { + line3 = "Insert nano-SIM & restart/switch"; + } + drawPango(layoutSmall, rowSelected ? &colLight : &colMuted, + 10, cardY + 44, 18, line3, 220); + } + + // Quick Action Rows below the 2 SIM cards (4 rows, 24px each) + int actSlot = (simRowSel == 1) ? 2 : 1; + int otherSlot = (status.preferredSlot == 1) ? 2 : 1; + std::string quickRows[4] = { + std::string(kIconRefresh) + " Switch Active to SIM " + std::to_string(otherSlot), + std::string(kIconUnlock) + " Unlock SIM (PIN / PUK)", + std::string(kIconLock) + " Enable / Disable SIM PIN", + std::string(kIconKey) + " Change SIM PIN Code", + }; + (void)actSlot; + + int baseY = contentY + 142; + for (int r = 0; r < 4; ++r) { + int ry = baseY + r * 23; + bool isSel = (simRowSel == (r + 2)); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 22); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 10, ry, 22, quickRows[r], kWinWidth - 20); + } + + drawSoftkeyBar("Actions", "Select", "Close"); + } else if (activeTab == TabId::RadioInfo) { + // Modem summary card (height 102px) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 102); + XSetForeground(dpy, gc, pt.spec.border); + XDrawRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 9, 101); + + std::string stStr = status.mmPresent ? status.mmState : "nomodem"; + if (!status.mmFailedReason.empty()) { + stStr += " (" + status.mmFailedReason + ")"; + } + XftColor* stCol = &colRed; + if (status.mmState == "connected" || status.mmState == "registered") { + stCol = &colGreen; + } else if (status.mmState == "searching" || status.mmState == "connecting") { + stCol = &colOrange; + } + drawPango(layoutMain, stCol, 10, contentY + 2, 22, + "State: " + stStr, 185); + drawSignalBars(kWinWidth - 32, contentY + 6, status.signalQuality); + + std::string opLine = "Operator: " + + (status.operatorName.empty() ? "--" : status.operatorName); + if (!status.accessTech.empty()) { + opLine += " [" + status.accessTech + "]"; + } + drawPango(layoutSmall, &colLight, 10, contentY + 24, 18, opLine, 220); + + std::string sigLine = "Signal: " + std::to_string(status.signalQuality) + + "% | Slot: SIM " + + (status.primaryGwSlot > 0 ? std::to_string(status.primaryGwSlot) : "--"); + drawPango(layoutSmall, &colLight, 10, contentY + 42, 18, sigLine, 220); + + std::string imeiLine = "IMEI: " + (status.imei.empty() ? "--" : status.imei); + drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imeiLine, 220); + + std::string iccidLine = "ICCID: " + + (status.simIccid.empty() ? "--" : status.simIccid); + drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, iccidLine, 220); + + // 5 Interactive Radio Control Rows + std::string modeLabel = kRadioModes[status.modePresetIdx].label; + bool powerOn = (status.mmPowerState == "on" || status.mmState == "registered" || + status.mmState == "connected" || status.mmState == "searching"); + std::string radioRows[5] = { + std::string(kIconSignal) + " Mode: < " + modeLabel + " >", + std::string(kIconPhone) + " USSD Code (" + ussdInput + ")", + std::string(kIconFlash) + " Radio Power: " + (powerOn ? "ON" : "OFF"), + std::string(kIconRefresh) + " Restart ModemManager", + std::string(kIconInfo) + " Refresh Status Now", + }; + + int baseY = contentY + 106; + for (int r = 0; r < 5; ++r) { + int ry = baseY + r * 25; + bool isSel = (radioRowSel == r); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 24); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 10, ry, 24, radioRows[r], kWinWidth - 20); + } + + drawSoftkeyBar("Refresh", "Apply", "Close"); + } else if (activeTab == TabId::ModemLog) { + if (logRows.empty()) { + drawPango(layoutMain, &colMuted, 16, contentY + 60, 24, + "No modem events logged."); + } else { + const int rowH = 38; + const int maxVis = contentH / rowH; // 6 rows + int startIdx = std::clamp(logRowSel - maxVis / 2, 0, + std::max(0, static_cast(logRows.size()) - maxVis)); + for (int i = 0; i < maxVis && (startIdx + i) < static_cast(logRows.size()); ++i) { + int idx = startIdx + i; + const auto& row = logRows[idx]; + int ry = contentY + i * rowH; + bool isSel = (idx == logRowSel); + + XSetForeground(dpy, gc, isSel ? pt.spec.selBg + : ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1); + + const char* ic = kIconInfo; + XftColor* icCol = &colBlue; + if (row.eventType.find("missed") != std::string::npos) { + ic = kIconMissedCall; + icCol = &colRed; + } else if (row.eventType.find("call_") != std::string::npos) { + ic = kIconPhone; + icCol = &colGreen; + } else if (row.eventType.find("txt") != std::string::npos) { + ic = kIconSms; + icCol = &colYellow; + } + drawPango(layoutIcon, icCol, 6, ry + 2, 18, ic); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 26, ry + 2, 18, row.eventType + " | " + row.timeStr, 208); + std::string sub = row.target; + if (!row.detail.empty()) sub += " - " + row.detail; + drawPango(layoutSmall, isSel ? &colLight : &colMuted, + 26, ry + 19, 17, sub, 208); + } + } + drawSoftkeyBar("Refresh", "Call/SMS", "Close"); + } + + // Modal Overlays + if (modal == ModalMode::SimActions) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 12, 44, kWinWidth - 24, 196); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 12, 44, kWinWidth - 25, 195); + + std::string hdr = "SIM Slot " + std::to_string(targetSlot) + " Actions"; + drawPango(layoutMain, &colYellow, 20, 48, 24, hdr, kWinWidth - 40); + + const char* actLabels[5] = { + "Set as Active SIM Slot", + "Unlock SIM (Enter PIN)", + "Unblock SIM (Enter PUK)", + "Enable / Disable SIM PIN", + "Change SIM PIN Code", + }; + for (int i = 0; i < 5; ++i) { + int ry = 76 + i * 30; + bool isSel = (actionSel == i); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton); + XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 26); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 24, ry, 26, actLabels[i], kWinWidth - 48); + } + drawSoftkeyBar("Select", "OK", "Back"); + } else if (modal == ModalMode::VerifyPin || + modal == ModalMode::UnblockPuk || + modal == ModalMode::TogglePinLock || + modal == ModalMode::ChangePin) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 10, 40, kWinWidth - 20, 210); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 10, 40, kWinWidth - 21, 209); + + const auto& s = status.slots[targetSlot - 1]; + std::string title; + std::string prompt; + std::string activeInput; + std::string retriesText; + + if (modal == ModalMode::VerifyPin) { + title = "Unlock SIM " + std::to_string(targetSlot) + " (PIN)"; + prompt = "Enter 4-8 digit SIM PIN:"; + activeInput = maskPinStr(inputPin1); + retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries); + } else if (modal == ModalMode::UnblockPuk) { + title = "Unblock SIM " + std::to_string(targetSlot) + " (PUK)"; + retriesText = "PUK attempts remaining: " + std::to_string(s.puk1Retries); + if (wizardStep == 0) { + prompt = "Step 1/3: Enter 8-digit PUK:"; + activeInput = revealDigits ? inputPin1 : inputPin1; // PUK is usually visible or toggleable + } else if (wizardStep == 1) { + prompt = "Step 2/3: Enter New PIN (4-8 digits):"; + activeInput = maskPinStr(inputPin2); + } else { + prompt = "Step 3/3: Confirm New PIN:"; + activeInput = maskPinStr(inputPin3); + } + } else if (modal == ModalMode::TogglePinLock) { + bool enableTarget = !s.isPinEnabled(); + title = (enableTarget ? "Enable" : "Disable") + + std::string(" SIM ") + std::to_string(targetSlot) + " PIN"; + prompt = "Enter current SIM PIN:"; + activeInput = maskPinStr(inputPin1); + retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries); + } else if (modal == ModalMode::ChangePin) { + title = "Change SIM " + std::to_string(targetSlot) + " PIN"; + retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries); + if (wizardStep == 0) { + prompt = "Step 1/3: Current SIM PIN:"; + activeInput = maskPinStr(inputPin1); + } else if (wizardStep == 1) { + prompt = "Step 2/3: New SIM PIN (4-8 digits):"; + activeInput = maskPinStr(inputPin2); + } else { + prompt = "Step 3/3: Confirm New SIM PIN:"; + activeInput = maskPinStr(inputPin3); + } + } + + drawPango(layoutMain, &colYellow, 18, 46, 24, title, kWinWidth - 36); + drawPango(layoutSmall, (s.pin1Retries <= 1 && s.pin1Retries > 0) ? &colRed : &colOrange, + 18, 72, 20, retriesText, kWinWidth - 36); + drawPango(layoutSmall, &colLight, 18, 98, 20, prompt, kWinWidth - 36); + + // Input box + XSetForeground(dpy, gc, pt.spec.bgInputFocus); + XFillRectangle(dpy, pix, gc, 18, 122, kWinWidth - 36, 34); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 18, 122, kWinWidth - 37, 33); + + std::string dispBox = activeInput + "_"; + drawPango(layoutMain, &colWhite, 26, 122, 34, dispBox, kWinWidth - 52); + + drawPango(layoutSmall, &colMuted, 18, 164, 18, + "Press [*] to Show/Hide digits", kWinWidth - 36); + drawPango(layoutSmall, &colMuted, 18, 182, 18, + "Press [BackSpace] to delete digit", kWinWidth - 36); + + if (!modalError.empty()) { + drawPango(layoutSmall, &colRed, 18, 204, 20, modalError, kWinWidth - 36); + } + + drawSoftkeyBar("Submit", "OK", "Cancel"); + } else if (modal == ModalMode::UssdDialog) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 10, 40, kWinWidth - 20, 210); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 10, 40, kWinWidth - 21, 209); + + drawPango(layoutMain, &colYellow, 18, 46, 24, + ussdWaitingReply ? "USSD Reply" : "Send USSD Code", kWinWidth - 36); + + XSetForeground(dpy, gc, pt.spec.bgInputFocus); + XFillRectangle(dpy, pix, gc, 18, 74, kWinWidth - 36, 30); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 18, 74, kWinWidth - 37, 29); + drawPango(layoutMain, &colWhite, 24, 74, 30, ussdInput + "_", kWinWidth - 48); + + // Response area + XSetForeground(dpy, gc, pt.spec.bgInput); + XFillRectangle(dpy, pix, gc, 18, 110, kWinWidth - 36, 130); + std::string respText = ussdResponse.empty() + ? "Dial e.g. *100# and press Send." + : ussdResponse; + pango_layout_set_width(layoutSmall, (kWinWidth - 48) * PANGO_SCALE); + pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); + pango_layout_set_text(layoutSmall, respText.c_str(), + static_cast(respText.size())); + pango_xft_render_layout(xftDraw, &colLight, layoutSmall, + 24 * PANGO_SCALE, 116 * PANGO_SCALE); + + drawSoftkeyBar("Send", "Send", "Close"); + } + + // Status Toast Banner + if (!statusBanner.empty() && nowMs() < statusBannerUntilMs) { + XSetForeground(dpy, gc, pt.spec.bgToast); + XFillRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 16, 26); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 17, 25); + drawPango(layoutSmall, &colWhite, 14, kWinHeight - 56, 26, + statusBanner, kWinWidth - 28); + } + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + auto grabKeys = [&]() { + for (int i = 0; i < 6; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, + CurrentTime) == GrabSuccess) { + break; + } + usleep(15000); + } + }; + + auto showWindow = [&](const std::string& arg = "") { + closeOtherPlugins("modem"); + refreshData(true); + applyOpenArg(arg); + writeIntFile(activePidPath, static_cast(::getpid())); + if (!visible) { + XMapRaised(dpy, win); + visible = true; + } else { + XRaiseWindow(dpy, win); + } + openStartMs = nowMs(); + startupIgnoreUntilMs = openStartMs + 180; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + grabKeys(); + render(); + }; + + auto hideWindow = [&]() { + if (!visible) return; + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + visible = false; + modal = ModalMode::None; + ::unlink(activePidPath.c_str()); + }; + + auto switchSimSlotNow = [&](int slotNum) { + slotNum = std::clamp(slotNum, 1, 2); + setBanner("Switching active SIM to Slot " + std::to_string(slotNum) + "..."); + render(); + int rc = 0; + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection switch " + + std::to_string(slotNum) + " >/dev/null 2>&1", &rc); + runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); + refreshData(false); + if (rc == 0) { + setBanner("Active SIM set to Slot " + std::to_string(slotNum)); + } else { + setBanner("Failed to switch SIM slot"); + } + }; + + auto submitModalInput = [&]() { + if (modal == ModalMode::VerifyPin) { + if (inputPin1.size() < 4 || inputPin1.size() > 8) { + modalError = "PIN must be 4 to 8 digits"; + return; + } + setBanner("Verifying SIM PIN..."); + render(); + int rc = -1; + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection verify-pin " + + std::to_string(targetSlot) + " " + shellQuote(inputPin1) + + " >/dev/null 2>&1", &rc); + refreshData(false); + runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); + if (rc == 0) { + modal = ModalMode::None; + setBanner("SIM " + std::to_string(targetSlot) + " unlocked"); + } else { + inputPin1.clear(); + modalError = "Incorrect PIN"; + } + } else if (modal == ModalMode::UnblockPuk) { + if (wizardStep == 0) { + if (inputPin1.size() != 8) { + modalError = "PUK must be 8 digits"; + return; + } + modalError.clear(); + wizardStep = 1; + } else if (wizardStep == 1) { + if (inputPin2.size() < 4 || inputPin2.size() > 8) { + modalError = "New PIN must be 4-8 digits"; + return; + } + modalError.clear(); + wizardStep = 2; + } else { + if (inputPin3 != inputPin2) { + inputPin3.clear(); + modalError = "PIN confirmation does not match"; + return; + } + setBanner("Unblocking SIM with PUK..."); + render(); + int rc = -1; + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection unblock-puk " + + std::to_string(targetSlot) + " " + shellQuote(inputPin1) + + " " + shellQuote(inputPin2) + " >/dev/null 2>&1", &rc); + refreshData(false); + runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); + if (rc == 0) { + modal = ModalMode::None; + setBanner("SIM unblocked and new PIN set"); + } else { + wizardStep = 0; + inputPin1.clear(); + inputPin2.clear(); + inputPin3.clear(); + modalError = "PUK unblock failed"; + } + } + } else if (modal == ModalMode::TogglePinLock) { + if (inputPin1.size() < 4 || inputPin1.size() > 8) { + modalError = "PIN must be 4 to 8 digits"; + return; + } + bool enableTarget = !status.slots[targetSlot - 1].isPinEnabled(); + std::string modeStr = enableTarget ? "enable" : "disable"; + setBanner((enableTarget ? "Enabling" : "Disabling") + std::string(" SIM PIN...")); + render(); + int rc = -1; + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection set-pin-lock " + + std::to_string(targetSlot) + " " + modeStr + " " + + shellQuote(inputPin1) + " >/dev/null 2>&1", &rc); + refreshData(false); + if (rc == 0) { + modal = ModalMode::None; + setBanner(std::string("SIM PIN protection ") + + (enableTarget ? "enabled" : "disabled")); + } else { + inputPin1.clear(); + modalError = "Failed (wrong PIN?)"; + } + } else if (modal == ModalMode::ChangePin) { + if (wizardStep == 0) { + if (inputPin1.size() < 4 || inputPin1.size() > 8) { + modalError = "Current PIN must be 4-8 digits"; + return; + } + modalError.clear(); + wizardStep = 1; + } else if (wizardStep == 1) { + if (inputPin2.size() < 4 || inputPin2.size() > 8) { + modalError = "New PIN must be 4-8 digits"; + return; + } + modalError.clear(); + wizardStep = 2; + } else { + if (inputPin3 != inputPin2) { + inputPin3.clear(); + modalError = "New PINs do not match"; + return; + } + setBanner("Changing SIM PIN..."); + render(); + int rc = -1; + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection change-pin " + + std::to_string(targetSlot) + " " + shellQuote(inputPin1) + + " " + shellQuote(inputPin2) + " >/dev/null 2>&1", &rc); + refreshData(false); + if (rc == 0) { + modal = ModalMode::None; + setBanner("SIM PIN changed successfully"); + } else { + wizardStep = 0; + inputPin1.clear(); + inputPin2.clear(); + inputPin3.clear(); + modalError = "Failed to change PIN"; + } + } + } else if (modal == ModalMode::UssdDialog) { + if (ussdInput.empty()) return; + ussdResponse = "Sending USSD request..."; + render(); + std::string cmd = ussdWaitingReply + ? ("mmcli -m any --3gpp-ussd-respond=" + shellQuote(ussdInput) + " 2>&1") + : ("mmcli -m any --3gpp-ussd-initiate=" + shellQuote(ussdInput) + " 2>&1"); + int rc = 0; + std::string out = trimStr(runCaptureCmd(cmd, &rc)); + ussdResponse = out.empty() ? "(No response)" : out; + std::string stOut = runCaptureCmd("mmcli -m any --3gpp-ussd-status -K 2>/dev/null"); + ussdWaitingReply = (stOut.find("user-response") != std::string::npos); + if (ussdWaitingReply) { + ussdInput.clear(); + } + } + }; + + auto applyRadioModePreset = [&](int presetIdx) { + presetIdx = (presetIdx + kNumRadioModes) % kNumRadioModes; + status.modePresetIdx = presetIdx; + const auto& p = kRadioModes[presetIdx]; + setBanner(std::string("Setting mode: ") + p.label); + render(); + std::string cmd = "mmcli -m any --set-allowed-modes=" + + shellQuote(p.allowed); + if ( std::strcmp(p.preferred, "none") != 0) { + cmd += " --set-preferred-mode=" + shellQuote(p.preferred); + } + cmd += " >/dev/null 2>&1"; + int rc = 0; + runCaptureCmd(cmd, &rc); + refreshData(false); + setBanner(rc == 0 ? (std::string("Mode: ") + p.label) : "Mode change failed"); + }; + + auto triggerSimRowAction = [&](int row) { + if (row == 0 || row == 1) { + targetSlot = row + 1; + actionSel = 0; + modal = ModalMode::SimActions; + } else if (row == 2) { + int otherSlot = (status.preferredSlot == 1) ? 2 : 1; + switchSimSlotNow(otherSlot); + } else if (row == 3) { + int slot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) + ? status.primaryGwSlot + : status.preferredSlot; + openPinOrPukModalForSlot(slot); + } else if (row == 4) { + targetSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) + ? status.primaryGwSlot + : status.preferredSlot; + wizardStep = 0; + inputPin1.clear(); + modalError.clear(); + modal = ModalMode::TogglePinLock; + } else if (row == 5) { + targetSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2) + ? status.primaryGwSlot + : status.preferredSlot; + wizardStep = 0; + inputPin1.clear(); + inputPin2.clear(); + inputPin3.clear(); + modalError.clear(); + modal = ModalMode::ChangePin; + } + }; + + auto handleLeftSoftkey = [&]() { + if (modal == ModalMode::SimActions) { + int act = actionSel; + modal = ModalMode::None; + if (act == 0) { + switchSimSlotNow(targetSlot); + } else if (act == 1) { + wizardStep = 0; + inputPin1.clear(); + modalError.clear(); + modal = ModalMode::VerifyPin; + } else if (act == 2) { + wizardStep = 0; + inputPin1.clear(); + inputPin2.clear(); + inputPin3.clear(); + modalError.clear(); + modal = ModalMode::UnblockPuk; + } else if (act == 3) { + wizardStep = 0; + inputPin1.clear(); + modalError.clear(); + modal = ModalMode::TogglePinLock; + } else if (act == 4) { + wizardStep = 0; + inputPin1.clear(); + inputPin2.clear(); + inputPin3.clear(); + modalError.clear(); + modal = ModalMode::ChangePin; + } + return; + } + if (modal != ModalMode::None) { + submitModalInput(); + return; + } + if (activeTab == TabId::SimCards) { + targetSlot = (simRowSel == 1) ? 2 : 1; + actionSel = 0; + modal = ModalMode::SimActions; + } else if (activeTab == TabId::RadioInfo) { + refreshData(false); + setBanner("Modem status refreshed"); + } else if (activeTab == TabId::ModemLog) { + refreshData(true); + setBanner("Modem log refreshed"); + } + }; + + auto handleRightSoftkey = [&]() { + if (modal != ModalMode::None) { + if (modal == ModalMode::UssdDialog) { + runAsyncCmd("mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1"); + } + modal = ModalMode::None; + return; + } + hideWindow(); + }; + + auto handleEnterKey = [&]() { + if (modal == ModalMode::SimActions) { + handleLeftSoftkey(); + return; + } + if (modal != ModalMode::None) { + submitModalInput(); + return; + } + if (activeTab == TabId::SimCards) { + triggerSimRowAction(simRowSel); + } else if (activeTab == TabId::RadioInfo) { + if (radioRowSel == 0) { + applyRadioModePreset(status.modePresetIdx + 1); + } else if (radioRowSel == 1) { + modal = ModalMode::UssdDialog; + ussdResponse.clear(); + ussdWaitingReply = false; + } else if (radioRowSel == 2) { + bool powerOn = (status.mmPowerState == "on" || + status.mmState == "registered" || + status.mmState == "connected" || + status.mmState == "searching"); + setBanner(powerOn ? "Disabling modem..." : "Enabling modem..."); + render(); + runCaptureCmd(powerOn ? "mmcli -m any -d >/dev/null 2>&1" + : "mmcli -m any -e >/dev/null 2>&1"); + refreshData(false); + runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); + } else if (radioRowSel == 3) { + setBanner("Restarting ModemManager..."); + render(); + runCaptureCmd("doas -n rc-service modemmanager restart >/dev/null 2>&1"); + refreshData(false); + setBanner("ModemManager restarted"); + } else if (radioRowSel == 4) { + refreshData(false); + setBanner("Refreshed"); + } + } else if (activeTab == TabId::ModemLog) { + if (!logRows.empty() && logRowSel >= 0 && + logRowSel < static_cast(logRows.size())) { + const auto& r = logRows[logRowSel]; + std::string num = normalizePhone(r.target); + if (!num.empty()) { + if (r.eventType.find("txt") != std::string::npos) { + hideWindow(); + if (!sendPluginCommand("sms", "OPEN thread:" + num)) { + runAsyncArgv("xsms", "tailtextlog", num.c_str()); + } + } else { + hideWindow(); + if (!sendPluginCommand("dialer", "OPEN " + num)) { + runAsyncArgv("xdialer", num.c_str()); + } + } + } + } + } + }; + + if (!isPreload) { + showWindow(openArg); + } + + int xfd = ConnectionNumber(dpy); + while (gRunning) { + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == Expose && ev.xexpose.count == 0) { + render(); + } else if (ev.type == VisibilityNotify && visible && + ev.xvisibility.state != VisibilityUnobscured) { + XRaiseWindow(dpy, win); + } else if (ev.type == KeyRelease) { + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + } else if (ev.type == KeyPress && visible) { + int64_t now = nowMs(); + if (now < startupIgnoreUntilMs) continue; + + KeySym sym = XLookupKeysym(&ev.xkey, 0); + unsigned int kc = ev.xkey.keycode; + + bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu || + sym == XF86XK_TaskPane); + bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back || + sym == XK_Escape || + sym == static_cast(XF86XK_KbdLcdMenu2)); + + if (isLSK || isRSK) { + if (!initialSoftkeysReleased) continue; + int mask = querySoftkeysDown(dpy); + if (mask == 3 || (softkeyPending != 0 && + ((isLSK && softkeyPending == 2) || + (isRSK && softkeyPending == 1)))) { + softkeyPending = 0; + writeSoftkeySuppress(now + 400); + hideWindow(); + sendPluginCommand("xdrawer", "TOGGLE"); + continue; + } + softkeyPending = isLSK ? 1 : 2; + softkeyDeadlineMs = now + 90; + continue; + } + + if (sym == XF86XK_HangupPhone || sym == XK_Cancel) { + hideWindow(); + continue; + } + + // Modal numeric / code input handling + if (modal == ModalMode::VerifyPin || + modal == ModalMode::UnblockPuk || + modal == ModalMode::TogglePinLock || + modal == ModalMode::ChangePin) { + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '*') { + revealDigits = !revealDigits; + render(); + continue; + } + std::string* curStr = &inputPin1; + size_t maxLen = (modal == ModalMode::UnblockPuk && wizardStep == 0) ? 8 : 8; + if ((modal == ModalMode::UnblockPuk || modal == ModalMode::ChangePin) && + wizardStep == 1) { + curStr = &inputPin2; + } else if ((modal == ModalMode::UnblockPuk || modal == ModalMode::ChangePin) && + wizardStep == 2) { + curStr = &inputPin3; + } + if (dch >= '0' && dch <= '9') { + if (curStr->size() < maxLen) { + curStr->push_back(dch); + modalError.clear(); + render(); + } + continue; + } + if (sym == XK_BackSpace || sym == XK_Delete) { + if (!curStr->empty()) { + curStr->pop_back(); + modalError.clear(); + render(); + } else if (wizardStep > 0) { + --wizardStep; + render(); + } + continue; + } + if (sym == XK_Return || sym == XK_KP_Enter) { + submitModalInput(); + render(); + continue; + } + continue; + } + + if (modal == ModalMode::UssdDialog) { + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch != '\0') { + if (ussdInput.size() < 32) { + ussdInput.push_back(dch); + render(); + } + continue; + } + if (sym == XK_BackSpace || sym == XK_Delete) { + if (!ussdInput.empty()) { + ussdInput.pop_back(); + render(); + } + continue; + } + if (sym == XK_Return || sym == XK_KP_Enter || + sym == XF86XK_Send || sym == XF86XK_Phone || + sym == XF86XK_PickupPhone) { + submitModalInput(); + render(); + continue; + } + continue; + } + + if (modal == ModalMode::SimActions) { + if (sym == XK_Up || sym == XK_KP_Up) { + actionSel = (actionSel + 4) % 5; + render(); + } else if (sym == XK_Down || sym == XK_KP_Down) { + actionSel = (actionSel + 1) % 5; + render(); + } else if (sym == XK_Return || sym == XK_KP_Enter) { + handleEnterKey(); + render(); + } + continue; + } + + // Main View Navigation + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '1') { + activeTab = TabId::SimCards; + render(); + continue; + } else if (dch == '2') { + activeTab = TabId::RadioInfo; + render(); + continue; + } else if (dch == '3') { + activeTab = TabId::ModemLog; + refreshData(true); + render(); + continue; + } + + if (sym == XK_Left || sym == XK_KP_Left) { + if (activeTab == TabId::RadioInfo && radioRowSel == 0) { + applyRadioModePreset(status.modePresetIdx - 1); + } else { + activeTab = static_cast((static_cast(activeTab) + 2) % 3); + if (activeTab == TabId::ModemLog) refreshData(true); + } + render(); + } else if (sym == XK_Right || sym == XK_KP_Right) { + if (activeTab == TabId::RadioInfo && radioRowSel == 0) { + applyRadioModePreset(status.modePresetIdx + 1); + } else { + activeTab = static_cast((static_cast(activeTab) + 1) % 3); + if (activeTab == TabId::ModemLog) refreshData(true); + } + render(); + } else if (sym == XK_Up || sym == XK_KP_Up) { + if (activeTab == TabId::SimCards) { + simRowSel = (simRowSel + 5) % 6; + } else if (activeTab == TabId::RadioInfo) { + radioRowSel = (radioRowSel + 4) % 5; + } else if (activeTab == TabId::ModemLog && !logRows.empty()) { + logRowSel = std::max(0, logRowSel - 1); + } + render(); + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (activeTab == TabId::SimCards) { + simRowSel = (simRowSel + 1) % 6; + } else if (activeTab == TabId::RadioInfo) { + radioRowSel = (radioRowSel + 1) % 5; + } else if (activeTab == TabId::ModemLog && !logRows.empty()) { + logRowSel = std::min(static_cast(logRows.size()) - 1, logRowSel + 1); + } + render(); + } else if (sym == XK_Return || sym == XK_KP_Enter) { + handleEnterKey(); + render(); + } + } + } + + int timeoutMs = -1; + int64_t now = nowMs(); + if (visible) { + if (softkeyPending != 0) { + int64_t rem = softkeyDeadlineMs - now; + if (rem <= 0) { + int pend = softkeyPending; + softkeyPending = 0; + if (querySoftkeysDown(dpy) == 3) { + writeSoftkeySuppress(now + 400); + hideWindow(); + sendPluginCommand("xdrawer", "TOGGLE"); + } else if (pend == 1) { + handleLeftSoftkey(); + render(); + } else if (pend == 2) { + handleRightSoftkey(); + render(); + } + } else { + timeoutMs = static_cast(rem); + } + } + if (!statusBanner.empty()) { + int64_t bRem = statusBannerUntilMs - now; + if (bRem <= 0) { + statusBanner.clear(); + render(); + } else if (timeoutMs < 0 || bRem < timeoutMs) { + timeoutMs = static_cast(bRem); + } + } + // Periodic 5-second status refresh while visible on SIM/Radio tabs + if (modal == ModalMode::None && activeTab != TabId::ModemLog) { + int64_t rRem = (lastRefreshMs + 5000) - now; + if (rRem <= 0) { + refreshData(false); + render(); + rRem = 5000; + } + if (timeoutMs < 0 || rRem < timeoutMs) { + timeoutMs = static_cast(rRem); + } + } + } + + struct pollfd pfds[2]; + pfds[0].fd = xfd; + pfds[0].events = POLLIN; + pfds[1].fd = fifoFd; + pfds[1].events = POLLIN; + + int pr = ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs); + if (pr > 0 && fifoFd >= 0 && (pfds[1].revents & POLLIN)) { + for (const auto& cmd : readFifoLines(fifoFd)) { + if (cmd == "CLOSE") { + hideWindow(); + } else if (cmd == "RELOAD_THEME") { + pt.reload(); + if (visible) render(); + } else if (cmd == "TOGGLE") { + if (visible) hideWindow(); + else showWindow(); + } else if (cmd.rfind("OPEN", 0) == 0) { + std::string arg = (cmd.size() > 5) ? trimStr(cmd.substr(5)) : ""; + showWindow(arg); + } + } + } + } + + hideWindow(); + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + XftDrawDestroy(xftDraw); + XFreePixmap(dpy, pix); + XFreeGC(dpy, gc); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} + +int pluginInit(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("modem", "--preload"); + } + return 0; +} + +void pluginCleanup(void) {} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "modem", + "Bananarchy Dual-SIM, PIN/PUK & Modem Manager (240x320)", + pluginInit, + pluginCleanup, + nullptr, + nullptr, + nullptr, + runModemApp +}; diff --git a/src/dwm-plugins/networks_plugin.cpp b/src/dwm-plugins/networks_plugin.cpp new file mode 100644 index 0000000..2cbf112 --- /dev/null +++ b/src/dwm-plugins/networks_plugin.cpp @@ -0,0 +1,1503 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Bananarchy Networks & Bluetooth Manager Plugin (networks.so / xnetworks) + +#include "phone_data.h" +#include "../sxmo-dwm/dwm_plugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef None +#undef None +#endif + +namespace { + +using namespace bananarchy; + +volatile sig_atomic_t gRunning = 1; + +void handleSignal(int) { + gRunning = 0; +} + +enum class NetTab { + Wifi = 0, + Cellular = 1, + Bluetooth = 2, +}; + +enum class NetModal { + None = 0, + WifiActions = 1, + WifiPassword = 2, + ApnEditor = 3, + BtActions = 4, +}; + +enum class TextMode { + Lower = 0, // abc + Title = 1, // Abc + Upper = 2, // ABC + Num = 3, // 123 + Sym = 4, // Sym +}; + +const char* kMultiTapMap[10] = { + " 0+_-", // 0 + ".,?!@-_1:/#", // 1 + "abc2", // 2 + "def3", // 3 + "ghi4", // 4 + "jkl5", // 5 + "mno6", // 6 + "pqrs7", // 7 + "tuv8", // 8 + "wxyz9" // 9 +}; + +const char* kSymTapMap[10] = { + " 0+=", // 0 + ".,?!@1", // 1 + "_-/:;2", // 2 + "#*&$%3", // 3 + "()[]{}4", // 4 + "<>\"'`5", // 5 + "\\|^~6", // 6 + "+-=*7", // 7 + "$,.:8", // 8 + "!?;:9" // 9 +}; + +void popUtf8Char(std::string& s) { + if (s.empty()) return; + while (!s.empty() && (static_cast(s.back()) & 0xC0) == 0x80) { + s.pop_back(); + } + if (!s.empty()) s.pop_back(); +} + +std::string shellQuote(const std::string& s) { + std::string out = "'"; + for (char c : s) { + if (c == '\'') { + out += "'\\''"; + } else { + out.push_back(c); + } + } + out.push_back('\''); + return out; +} + +struct WifiApEntry { + std::string ssid; + int signal = 0; + std::string security; + bool inUse = false; + bool isSaved = false; +}; + +struct CellularNetInfo { + bool gsmConfigured = false; + bool gsmActive = false; + std::string gsmConName; + std::string apn; + std::string username; + std::string ipType = "ipv4v6"; + std::string wwanIp; + std::string wlanIp; + bool hotspotActive = false; + std::string hotspotInfo; + bool usbShareActive = false; +}; + +struct BtDeviceEntry { + std::string mac; + std::string name; + bool paired = false; + bool connected = false; +}; + +bool isWifiEnabledFast() { + std::string out = runCaptureCmd("/usr/sbin/rfkill list wifi 2>/dev/null"); + if (out.find("Soft blocked: yes") != std::string::npos || + out.find("Hard blocked: yes") != std::string::npos) { + return false; + } + std::string nm = trimStr(runCaptureCmd("nmcli radio wifi 2>/dev/null")); + if (nm == "disabled") return false; + return true; +} + +bool isBtEnabledFast() { + std::string out = runCaptureCmd("/usr/sbin/rfkill list bluetooth 2>/dev/null"); + if (out.find("Soft blocked: yes") != std::string::npos || + out.find("Hard blocked: yes") != std::string::npos) { + return false; + } + return true; +} + +std::vector queryWifiList(bool forceRescan) { + std::vector list; + std::unordered_set savedSsids; + + std::string conOut = runCaptureCmd( + "nmcli -t -f NAME,TYPE connection show 2>/dev/null"); + { + std::istringstream ss(conOut); + std::string line; + while (std::getline(ss, line)) { + size_t col = line.rfind(':'); + if (col == std::string::npos) continue; + std::string nm = line.substr(0, col); + std::string tp = line.substr(col + 1); + if (tp.find("wireless") != std::string::npos && !nm.empty()) { + savedSsids.insert(nm); + } + } + } + + std::string cmd = std::string("nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan ") + + (forceRescan ? "yes" : "no") + " 2>/dev/null"; + std::string out = runCaptureCmd(cmd); + std::istringstream ss(out); + std::string line; + std::unordered_map seenIdx; + + while (std::getline(ss, line)) { + if (line.empty()) continue; + // Parse IN-USE:SSID:SIGNAL:SECURITY (note SSID can contain escaped colons \:) + size_t c1 = line.find(':'); + if (c1 == std::string::npos) continue; + size_t c3 = line.rfind(':'); + if (c3 == std::string::npos || c3 <= c1) continue; + size_t c2 = line.rfind(':', c3 - 1); + if (c2 == std::string::npos || c2 <= c1) continue; + + std::string inUseStr = line.substr(0, c1); + std::string rawSsid = line.substr(c1 + 1, c2 - c1 - 1); + std::string sigStr = line.substr(c2 + 1, c3 - c2 - 1); + std::string secStr = line.substr(c3 + 1); + + std::string ssid; + for (size_t i = 0; i < rawSsid.size(); ++i) { + if (rawSsid[i] == '\\' && i + 1 < rawSsid.size()) { + ssid.push_back(rawSsid[++i]); + } else { + ssid.push_back(rawSsid[i]); + } + } + ssid = trimStr(ssid); + if (ssid.empty()) continue; + + WifiApEntry ap; + ap.ssid = ssid; + ap.signal = std::clamp(std::atoi(sigStr.c_str()), 0, 100); + ap.security = (secStr == "--") ? "" : trimStr(secStr); + ap.inUse = (inUseStr == "*"); + ap.isSaved = (savedSsids.count(ssid) > 0); + + auto it = seenIdx.find(ssid); + if (it == seenIdx.end()) { + seenIdx[ssid] = list.size(); + list.push_back(ap); + } else { + auto& existing = list[it->second]; + if (ap.inUse) existing.inUse = true; + if (ap.signal > existing.signal) existing.signal = ap.signal; + if (existing.security.empty() && !ap.security.empty()) { + existing.security = ap.security; + } + } + } + + std::sort(list.begin(), list.end(), + [](const WifiApEntry& a, const WifiApEntry& b) { + if (a.inUse != b.inUse) return a.inUse > b.inUse; + if (a.isSaved != b.isSaved) return a.isSaved > b.isSaved; + return a.signal > b.signal; + }); + return list; +} + +CellularNetInfo queryCellularInfo() { + CellularNetInfo info; + + std::string allCons = runCaptureCmd( + "nmcli -t -f NAME,TYPE,ACTIVE connection show 2>/dev/null"); + std::istringstream ss(allCons); + std::string line; + while (std::getline(ss, line)) { + size_t c2 = line.rfind(':'); + if (c2 == std::string::npos) continue; + size_t c1 = line.rfind(':', c2 - 1); + if (c1 == std::string::npos) continue; + std::string nm = line.substr(0, c1); + std::string tp = line.substr(c1 + 1, c2 - c1 - 1); + std::string act = line.substr(c2 + 1); + if (tp == "gsm" && info.gsmConName.empty()) { + info.gsmConfigured = true; + info.gsmConName = nm; + info.gsmActive = (act == "yes"); + } + if (nm == "Hotspot" && act == "yes") { + info.hotspotActive = true; + } + if (nm == "USBNetworkShare" && act == "yes") { + info.usbShareActive = true; + } + } + + if (info.gsmConfigured && !info.gsmConName.empty()) { + std::string det = runCaptureCmd( + "nmcli -t -f gsm.apn,gsm.username connection show " + + shellQuote(info.gsmConName) + " 2>/dev/null"); + std::istringstream ds(det); + std::string dline; + while (std::getline(ds, dline)) { + if (dline.rfind("gsm.apn:", 0) == 0) { + info.apn = trimStr(dline.substr(8)); + if (info.apn == "--") info.apn.clear(); + } else if (dline.rfind("gsm.username:", 0) == 0) { + info.username = trimStr(dline.substr(13)); + if (info.username == "--") info.username.clear(); + } + } + if (info.apn.empty()) info.apn = info.gsmConName; + } + + std::string ipOut = runCaptureCmd("ip -4 -o addr show 2>/dev/null"); + std::istringstream ips(ipOut); + while (std::getline(ips, line)) { + size_t inetPos = line.find("inet "); + if (inetPos == std::string::npos) continue; + std::string rest = line.substr(inetPos + 5); + size_t sp = rest.find(' '); + std::string cidr = (sp == std::string::npos) ? rest : rest.substr(0, sp); + if (line.find("wwan") != std::string::npos || + line.find("rmnet") != std::string::npos) { + info.wwanIp = cidr; + } else if (line.find("wlan") != std::string::npos) { + info.wlanIp = cidr; + } + } + + return info; +} + +std::vector queryBtDevices() { + std::vector out; + std::unordered_set pairedMacs; + std::unordered_set connectedMacs; + + auto parseMacSet = [](const std::string& raw, + std::unordered_set& dst) { + std::istringstream ss(raw); + std::string line; + while (std::getline(ss, line)) { + // Format: "Device XX:XX:XX:XX:XX:XX Name..." + if (line.rfind("Device ", 0) == 0 && line.size() >= 24) { + dst.insert(line.substr(7, 17)); + } + } + }; + + parseMacSet(runCaptureCmd("bluetoothctl devices Paired 2>/dev/null || " + "bluetoothctl paired-devices 2>/dev/null"), + pairedMacs); + parseMacSet(runCaptureCmd("bluetoothctl devices Connected 2>/dev/null"), + connectedMacs); + + std::string allRaw = runCaptureCmd("bluetoothctl devices 2>/dev/null"); + std::istringstream ss(allRaw); + std::string line; + while (std::getline(ss, line)) { + if (line.rfind("Device ", 0) != 0 || line.size() < 24) continue; + BtDeviceEntry d; + d.mac = line.substr(7, 17); + d.name = (line.size() > 25) ? trimStr(line.substr(25)) : d.mac; + d.paired = (pairedMacs.count(d.mac) > 0); + d.connected = (connectedMacs.count(d.mac) > 0); + out.push_back(d); + } + + std::sort(out.begin(), out.end(), + [](const BtDeviceEntry& a, const BtDeviceEntry& b) { + if (a.connected != b.connected) return a.connected > b.connected; + if (a.paired != b.paired) return a.paired > b.paired; + return a.name < b.name; + }); + return out; +} + +int runNetworksApp(int argc, char** argv) { + gRunning = 1; + std::setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + + bool isPreload = false; + std::string openArg; + + const char* base0 = std::strrchr(argv[0], '/'); + base0 = base0 ? (base0 + 1) : argv[0]; + if (base0 && (strcasecmp(base0, "xbluetooth") == 0 || + strcasecmp(base0, "bluetooth") == 0)) { + openArg = "bluetooth"; + } + + for (int i = 1; i < argc; ++i) { + if (!argv[i]) continue; + if (std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } else if (argv[i][0] != '-') { + openArg = argv[i]; + } + } + + std::string daemonPidPath = getPluginDaemonPidPath("networks"); + std::string activePidPath = getRuntimeFile("xnetworks.pid"); + + if (isPluginDaemonAlive("networks")) { + if (!isPreload) { + sendPluginCommand("networks", + openArg.empty() ? "OPEN" : ("OPEN " + openArg)); + } + return 0; + } + + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("networks"); + + struct sigaction sa {}; + sa.sa_handler = handleSignal; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) { + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + return 1; + } + + int screen = DefaultScreen(dpy); + Window root = RootWindow(dpy, screen); + Visual* visual = DefaultVisual(dpy, screen); + Colormap cmap = DefaultColormap(dpy, screen); + + ThemeSpec initSpec; + initSpec.loadActive(dpy, screen); + + XSetWindowAttributes swa {}; + swa.override_redirect = True; + swa.background_pixel = initSpec.bgMain; + swa.border_pixel = initSpec.selBg; + swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | + VisibilityChangeMask | StructureNotifyMask; + + Window win = XCreateWindow( + dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, + DefaultDepth(dpy, screen), CopyFromParent, visual, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); + + XClassHint ch {}; + char resName[] = "xnetworks"; + char resClass[] = "XNetworks"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Networks & Bluetooth"); + + Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, + DefaultDepth(dpy, screen)); + GC gc = XCreateGC(dpy, pix, 0, nullptr); + XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); + + PluginTheme pt; + pt.init(dpy, screen, visual, cmap, win); + auto& layoutMain = pt.layoutMain; + auto& layoutSmall = pt.layoutSmall; + auto& layoutIcon = pt.layoutIcon; + auto& colWhite = pt.colWhite; + auto& colLight = pt.colLight; + auto& colMuted = pt.colMuted; + auto& colYellow = pt.colYellow; + auto& colGreen = pt.colGreen; + auto& colBlue = pt.colBlue; + auto& colOrange = pt.colOrange; + auto& colRed = pt.colRed; + + NetTab activeTab = NetTab::Wifi; + NetModal modal = NetModal::None; + + bool wifiOn = true; + std::vector wifiList; + int wifiRowSel = 0; // 0 = Wi-Fi Toggle, 1..N = AP entries + + CellularNetInfo cellInfo; + int cellRowSel = 0; // 0 = Mobile Data, 1 = Configure APN, 2 = Wi-Fi Hotspot, 3 = USB Share + + bool btOn = false; + std::vector btList; + int btRowSel = 0; // 0 = BT Power Toggle, 1 = Scan Devices, 2..N = BT devices + + int actionSel = 0; + + // Wi-Fi Password modal state + WifiApEntry targetAp; + std::string wifiPassword; + bool showPassword = false; + + // APN Editor modal state + std::string apnName; + std::string apnUser; + std::string apnPass; + int apnFieldSel = 0; // 0 = APN, 1 = Username, 2 = Password, 3 = Save + + // Bluetooth target device + BtDeviceEntry targetBt; + + // Multi-tap state for text inputs + TextMode textMode = TextMode::Lower; + int lastTapDigit = -1; + int lastTapCycle = 0; + int64_t lastTapMs = 0; + + std::string statusBanner; + int64_t statusBannerUntilMs = 0; + + bool visible = false; + int64_t openStartMs = 0; + int64_t startupIgnoreUntilMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + auto setBanner = [&](const std::string& msg, int durationMs = 2300) { + statusBanner = msg; + statusBannerUntilMs = nowMs() + durationMs; + }; + + auto refreshCurrentTab = [&](bool forceScan = false) { + if (activeTab == NetTab::Wifi) { + wifiOn = isWifiEnabledFast(); + if (wifiOn) { + wifiList = queryWifiList(forceScan); + } else { + wifiList.clear(); + } + int maxRow = static_cast(wifiList.size()); + if (wifiRowSel > maxRow) wifiRowSel = maxRow; + } else if (activeTab == NetTab::Cellular) { + cellInfo = queryCellularInfo(); + } else if (activeTab == NetTab::Bluetooth) { + btOn = isBtEnabledFast(); + if (btOn) { + if (forceScan) { + runCaptureCmd("bluetoothctl --timeout 5 scan on >/dev/null 2>&1"); + } + btList = queryBtDevices(); + } else { + btList.clear(); + } + int maxRow = 1 + static_cast(btList.size()); + if (btRowSel > maxRow) btRowSel = maxRow; + } + }; + + auto applyOpenArg = [&](const std::string& arg) { + std::string a = toLowerStr(trimStr(arg)); + modal = NetModal::None; + if (a == "bluetooth" || a == "bt") { + activeTab = NetTab::Bluetooth; + } else if (a == "cell" || a == "cellular" || a == "apn" || a == "hotspot") { + activeTab = NetTab::Cellular; + } else { + activeTab = NetTab::Wifi; + } + refreshCurrentTab(false); + }; + + auto textModeLabel = [&]() -> const char* { + switch (textMode) { + case TextMode::Lower: return "abc"; + case TextMode::Title: return "Abc"; + case TextMode::Upper: return "ABC"; + case TextMode::Num: return "123"; + case TextMode::Sym: return "Sym"; + } + return "abc"; + }; + + auto handleMultiTapKey = [&](std::string& targetStr, KeySym sym, + unsigned int kc, unsigned int state) -> bool { + char dch = mapDialKey(sym, kc, state); + if (dch == '#') { + textMode = static_cast((static_cast(textMode) + 1) % 5); + lastTapDigit = -1; + return true; + } + if (dch >= '0' && dch <= '9') { + int digit = dch - '0'; + int64_t now = nowMs(); + if (textMode == TextMode::Num) { + targetStr.push_back(dch); + lastTapDigit = -1; + return true; + } + const char* seq = (textMode == TextMode::Sym) + ? kSymTapMap[digit] + : kMultiTapMap[digit]; + int seqLen = static_cast(std::strlen(seq)); + if (seqLen <= 0) return false; + + if (digit == lastTapDigit && (now - lastTapMs) < 750 && !targetStr.empty()) { + popUtf8Char(targetStr); + lastTapCycle = (lastTapCycle + 1) % seqLen; + } else { + lastTapDigit = digit; + lastTapCycle = 0; + } + lastTapMs = now; + + char outCh = seq[lastTapCycle]; + if (textMode == TextMode::Upper) { + outCh = static_cast(std::toupper(static_cast(outCh))); + } else if (textMode == TextMode::Title && targetStr.empty()) { + outCh = static_cast(std::toupper(static_cast(outCh))); + } + targetStr.push_back(outCh); + return true; + } + if (sym == XK_BackSpace || sym == XK_Delete) { + if (!targetStr.empty()) { + popUtf8Char(targetStr); + lastTapDigit = -1; + return true; + } + } + // Also accept direct ASCII keysyms from XIM / hardware keyboards + if (sym >= 0x20 && sym <= 0x7e && !(state & (ControlMask | Mod1Mask))) { + targetStr.push_back(static_cast(sym)); + lastTapDigit = -1; + return true; + } + return false; + }; + + auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, + const std::string& text, int maxW = -1) -> int { + pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); + if (maxW > 0) { + pango_layout_set_width(layout, maxW * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } else { + pango_layout_set_width(layout, -1); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE); + } + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + int dy = y + std::max(0, (h - th) / 2); + pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, + dy * PANGO_SCALE); + return tw; + }; + + auto pangoTextWidth = [&](PangoLayout* layout, const std::string& text) -> int { + pango_layout_set_width(layout, -1); + pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + return tw; + }; + + auto drawSoftkeyBar = [&](const std::string& leftLabel, + const std::string& centerLabel, + const std::string& rightLabel) { + const int barY = kWinHeight - 26; + XSetForeground(dpy, gc, pt.spec.bgFooter); + XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26); + XSetForeground(dpy, gc, pt.spec.divider); + XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); + + drawPango(layoutMain, &colBlue, 6, barY, 26, leftLabel); + int cw = pangoTextWidth(layoutMain, centerLabel); + drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26, centerLabel); + int rw = pangoTextWidth(layoutMain, rightLabel); + drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel); + }; + + auto render = [&]() { + if (!visible) return; + pt.checkAndReload(); + + XSetForeground(dpy, gc, pt.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // Top Tab Bar (Y=0..30) + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 30); + XSetForeground(dpy, gc, pt.spec.border); + XDrawLine(dpy, pix, gc, 0, 29, kWinWidth, 29); + + const char* tabLabels[3] = {"1:Wi-Fi", "2:Cell/APN", "3:Bluetooth"}; + int tabW = kWinWidth / 3; + for (int t = 0; t < 3; ++t) { + bool isSel = (static_cast(activeTab) == t); + if (isSel) { + XSetForeground(dpy, gc, pt.spec.selBg); + XFillRectangle(dpy, pix, gc, t * tabW, 0, tabW, 29); + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, t * tabW, 27, tabW, 2); + } + int tw = pangoTextWidth(layoutSmall, tabLabels[t]); + drawPango(layoutSmall, isSel ? &colWhite : &colMuted, + t * tabW + (tabW - tw) / 2, 0, 28, tabLabels[t]); + } + + const int contentY = 32; + const int contentH = kWinHeight - 32 - 26; + + if (activeTab == NetTab::Wifi) { + // Row 0: Wi-Fi Power Toggle (height 32) + bool sel0 = (wifiRowSel == 0); + XSetForeground(dpy, gc, sel0 ? pt.spec.selBg : pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 30); + drawPango(layoutIcon, wifiOn ? &colGreen : &colMuted, 10, contentY, 30, kIconWifi); + drawPango(layoutMain, sel0 ? &colWhite : &colLight, + 32, contentY, 30, std::string("Wi-Fi Radio: ") + (wifiOn ? "ON" : "OFF")); + drawPango(layoutIcon, wifiOn ? &colGreen : &colMuted, + kWinWidth - 28, contentY, 30, wifiOn ? kIconToggleOn : kIconToggleOff); + + int listY = contentY + 34; + int listH = contentH - 34; + if (!wifiOn) { + drawPango(layoutMain, &colMuted, 18, listY + 50, 24, + "Wi-Fi is turned off."); + } else if (wifiList.empty()) { + drawPango(layoutMain, &colMuted, 18, listY + 50, 24, + "No networks found. Press Scan."); + } else { + const int rowH = 38; + const int maxVis = listH / rowH; // 5 rows + int apSel = std::max(0, wifiRowSel - 1); + int startIdx = std::clamp(apSel - maxVis / 2, 0, + std::max(0, static_cast(wifiList.size()) - maxVis)); + for (int i = 0; i < maxVis && (startIdx + i) < static_cast(wifiList.size()); ++i) { + int idx = startIdx + i; + const auto& ap = wifiList[idx]; + int ry = listY + i * rowH; + bool isSel = (wifiRowSel == (idx + 1)); + + XSetForeground(dpy, gc, isSel ? pt.spec.selBg + : ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, rowH - 2); + + drawPango(layoutIcon, ap.inUse ? &colGreen : (ap.isSaved ? &colBlue : &colLight), + 10, ry + 1, 20, ap.security.empty() ? kIconWifi : kIconLock); + drawPango(layoutMain, isSel ? &colWhite : &colLight, + 30, ry + 1, 20, ap.ssid, kWinWidth - 42); + + std::string sub = std::to_string(ap.signal) + "% "; + if (ap.inUse) sub += "[Connected] "; + else if (ap.isSaved) sub += "[Saved] "; + sub += ap.security.empty() ? "Open" : ap.security; + drawPango(layoutSmall, ap.inUse ? &colGreen : (isSel ? &colLight : &colMuted), + 30, ry + 19, 17, sub, kWinWidth - 42); + } + } + drawSoftkeyBar("Scan", "Select", "Close"); + } else if (activeTab == NetTab::Cellular) { + // Status Card (height 86) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 86); + XSetForeground(dpy, gc, pt.spec.border); + XDrawRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 9, 85); + + std::string cellSt = cellInfo.gsmActive ? "Connected" + : (cellInfo.gsmConfigured ? "Disconnected" : "Unconfigured"); + drawPango(layoutMain, cellInfo.gsmActive ? &colGreen : &colOrange, + 10, contentY + 2, 22, "Cellular: " + cellSt, 220); + drawPango(layoutSmall, &colLight, 10, contentY + 24, 18, + "APN: " + (cellInfo.apn.empty() ? "(not set)" : cellInfo.apn), 220); + drawPango(layoutSmall, &colMuted, 10, contentY + 43, 18, + "WWAN IP: " + (cellInfo.wwanIp.empty() ? "--" : cellInfo.wwanIp), 220); + drawPango(layoutSmall, &colMuted, 10, contentY + 62, 18, + "WLAN IP: " + (cellInfo.wlanIp.empty() ? "--" : cellInfo.wlanIp), 220); + + // 4 Interactive Rows + std::string rows[4] = { + std::string(kIconGlobe) + " Mobile Data: " + (cellInfo.gsmActive ? "ON" : "OFF"), + std::string(kIconEdit) + " Configure APN (" + (cellInfo.apn.empty() ? "Set" : cellInfo.apn) + ")", + std::string(kIconWifi) + " Wi-Fi Hotspot: " + (cellInfo.hotspotActive ? "ON" : "OFF"), + std::string(kIconFlash) + " USB Tethering: " + (cellInfo.usbShareActive ? "ON" : "OFF"), + }; + int baseY = contentY + 92; + for (int r = 0; r < 4; ++r) { + int ry = baseY + r * 32; + bool isSel = (cellRowSel == r); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg + : ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 30); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 10, ry, 30, rows[r], kWinWidth - 20); + } + drawSoftkeyBar("Refresh", "Toggle/Edit", "Close"); + } else if (activeTab == NetTab::Bluetooth) { + // Row 0: BT Power Toggle (height 28) + bool sel0 = (btRowSel == 0); + XSetForeground(dpy, gc, sel0 ? pt.spec.selBg : pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 28); + drawPango(layoutIcon, btOn ? &colBlue : &colMuted, 10, contentY, 28, kIconBluetooth); + drawPango(layoutMain, sel0 ? &colWhite : &colLight, + 30, contentY, 28, std::string("Bluetooth: ") + (btOn ? "ON" : "OFF")); + drawPango(layoutIcon, btOn ? &colGreen : &colMuted, + kWinWidth - 28, contentY, 28, btOn ? kIconToggleOn : kIconToggleOff); + + // Row 1: Scan for Devices (height 26) + bool sel1 = (btRowSel == 1); + XSetForeground(dpy, gc, sel1 ? pt.spec.selBg : pt.spec.bgAltOdd); + XFillRectangle(dpy, pix, gc, 4, contentY + 30, kWinWidth - 8, 26); + drawPango(layoutIcon, &colYellow, 10, contentY + 30, 26, kIconSearch); + drawPango(layoutSmall, sel1 ? &colWhite : &colLight, + 30, contentY + 30, 26, "Scan for Bluetooth Devices"); + + int listY = contentY + 58; + int listH = contentH - 58; + if (!btOn) { + drawPango(layoutMain, &colMuted, 18, listY + 40, 24, + "Bluetooth is turned off."); + } else if (btList.empty()) { + drawPango(layoutMain, &colMuted, 18, listY + 40, 24, + "No devices. Select Scan."); + } else { + const int rowH = 38; + const int maxVis = listH / rowH; // 4 rows + int devSel = std::max(0, btRowSel - 2); + int startIdx = std::clamp(devSel - maxVis / 2, 0, + std::max(0, static_cast(btList.size()) - maxVis)); + for (int i = 0; i < maxVis && (startIdx + i) < static_cast(btList.size()); ++i) { + int idx = startIdx + i; + const auto& d = btList[idx]; + int ry = listY + i * rowH; + bool isSel = (btRowSel == (idx + 2)); + + XSetForeground(dpy, gc, isSel ? pt.spec.selBg + : ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, rowH - 2); + + drawPango(layoutIcon, d.connected ? &colGreen : (d.paired ? &colBlue : &colMuted), + 10, ry + 1, 20, kIconBluetooth); + drawPango(layoutMain, isSel ? &colWhite : &colLight, + 30, ry + 1, 20, d.name, kWinWidth - 42); + + std::string st = d.connected ? "[Connected] " + : (d.paired ? "[Paired] " : "[Available] "); + st += d.mac; + drawPango(layoutSmall, d.connected ? &colGreen : (isSel ? &colLight : &colMuted), + 30, ry + 19, 17, st, kWinWidth - 42); + } + } + drawSoftkeyBar("Scan", "Select", "Close"); + } + + // Modal Overlays + if (modal == NetModal::WifiActions) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 12, 48, kWinWidth - 24, 174); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 12, 48, kWinWidth - 25, 173); + + drawPango(layoutMain, &colYellow, 20, 52, 24, targetAp.ssid, kWinWidth - 40); + const char* acts[4] = { + targetAp.inUse ? "Reconnect Network" : "Connect to Network", + "Enter / Update Password", + "Disconnect Network", + "Forget Saved Network", + }; + for (int i = 0; i < 4; ++i) { + int ry = 82 + i * 32; + bool isSel = (actionSel == i); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton); + XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 28); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 24, ry, 28, acts[i], kWinWidth - 48); + } + drawSoftkeyBar("Select", "OK", "Back"); + } else if (modal == NetModal::WifiPassword) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 10, 42, kWinWidth - 20, 200); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 10, 42, kWinWidth - 21, 199); + + drawPango(layoutMain, &colYellow, 18, 48, 22, + "Wi-Fi: " + targetAp.ssid, kWinWidth - 80); + drawPango(layoutSmall, &colBlue, kWinWidth - 58, 48, 22, + std::string("[") + textModeLabel() + "]"); + + drawPango(layoutSmall, &colLight, 18, 76, 20, "Enter WPA/WEP Passphrase:"); + + XSetForeground(dpy, gc, pt.spec.bgInputFocus); + XFillRectangle(dpy, pix, gc, 18, 100, kWinWidth - 36, 34); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 18, 100, kWinWidth - 37, 33); + + std::string disp = showPassword ? wifiPassword + : std::string(wifiPassword.size(), '*'); + drawPango(layoutMain, &colWhite, 24, 100, 34, disp + "_", kWinWidth - 48); + + drawPango(layoutSmall, &colMuted, 18, 144, 18, + "[#] Cycle input mode (abc/Abc/ABC/123/Sym)"); + drawPango(layoutSmall, &colMuted, 18, 164, 18, + "[*] Toggle Show/Hide password"); + drawPango(layoutSmall, &colMuted, 18, 184, 18, + "[BackSpace] Delete character"); + + drawSoftkeyBar("Connect", "Connect", "Cancel"); + } else if (modal == NetModal::ApnEditor) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 10, 36, kWinWidth - 20, 220); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 10, 36, kWinWidth - 21, 219); + + drawPango(layoutMain, &colYellow, 18, 40, 22, "Cellular APN Setup"); + drawPango(layoutSmall, &colBlue, kWinWidth - 58, 40, 22, + std::string("[") + textModeLabel() + "]"); + + const char* labels[3] = {"APN Name:", "Username (optional):", "Password (optional):"}; + std::string vals[3] = {apnName, apnUser, apnPass}; + for (int f = 0; f < 3; ++f) { + int fy = 66 + f * 44; + bool isF = (apnFieldSel == f); + drawPango(layoutSmall, isF ? &colYellow : &colMuted, 18, fy, 16, labels[f]); + XSetForeground(dpy, gc, isF ? pt.spec.bgInputFocus : pt.spec.bgInput); + XFillRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 36, 24); + XSetForeground(dpy, gc, isF ? pt.spec.accent : pt.spec.border); + XDrawRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 37, 23); + drawPango(layoutSmall, &colWhite, 22, fy + 16, 24, + vals[f] + (isF ? "_" : ""), kWinWidth - 44); + } + + bool saveSel = (apnFieldSel == 3); + XSetForeground(dpy, gc, saveSel ? pt.spec.selBg : pt.spec.bgButton); + XFillRectangle(dpy, pix, gc, 18, 202, kWinWidth - 36, 26); + drawPango(layoutMain, saveSel ? &colWhite : &colGreen, + 50, 202, 26, "Save & Activate APN"); + + drawSoftkeyBar("Save", "Select", "Cancel"); + } else if (modal == NetModal::BtActions) { + XSetForeground(dpy, gc, pt.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 12, 48, kWinWidth - 24, 174); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 12, 48, kWinWidth - 25, 173); + + drawPango(layoutMain, &colYellow, 20, 52, 24, targetBt.name, kWinWidth - 40); + const char* acts[4] = { + "Connect Device", + "Disconnect Device", + "Pair & Trust Device", + "Forget / Remove Device", + }; + for (int i = 0; i < 4; ++i) { + int ry = 82 + i * 32; + bool isSel = (actionSel == i); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton); + XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 28); + drawPango(layoutSmall, isSel ? &colWhite : &colLight, + 24, ry, 28, acts[i], kWinWidth - 48); + } + drawSoftkeyBar("Select", "OK", "Back"); + } + + if (!statusBanner.empty() && nowMs() < statusBannerUntilMs) { + XSetForeground(dpy, gc, pt.spec.bgToast); + XFillRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 16, 26); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 17, 25); + drawPango(layoutSmall, &colWhite, 14, kWinHeight - 56, 26, + statusBanner, kWinWidth - 28); + } + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + auto grabKeys = [&]() { + for (int i = 0; i < 6; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, + CurrentTime) == GrabSuccess) { + break; + } + usleep(15000); + } + }; + + auto showWindow = [&](const std::string& arg = "") { + closeOtherPlugins("networks"); + applyOpenArg(arg); + writeIntFile(activePidPath, static_cast(::getpid())); + if (!visible) { + XMapRaised(dpy, win); + visible = true; + } else { + XRaiseWindow(dpy, win); + } + openStartMs = nowMs(); + startupIgnoreUntilMs = openStartMs + 180; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + grabKeys(); + render(); + }; + + auto hideWindow = [&]() { + if (!visible) return; + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + visible = false; + modal = NetModal::None; + ::unlink(activePidPath.c_str()); + }; + + auto connectWifiNow = [&](const std::string& ssid, const std::string& pass) { + modal = NetModal::None; + setBanner("Connecting to " + ssid + "..."); + render(); + int rc = 0; + std::string cmd = "nmcli device wifi connect " + shellQuote(ssid); + if (!pass.empty()) { + cmd += " password " + shellQuote(pass); + } + cmd += " >/dev/null 2>&1"; + runCaptureCmd(cmd, &rc); + refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1"); + setBanner(rc == 0 ? ("Connected to " + ssid) : ("Failed to connect to " + ssid)); + }; + + auto saveAndConnectApn = [&]() { + std::string cleanApn = trimStr(apnName); + if (cleanApn.empty()) { + setBanner("APN name cannot be empty"); + return; + } + modal = NetModal::None; + setBanner("Saving APN " + cleanApn + "..."); + render(); + if (cellInfo.gsmConfigured && !cellInfo.gsmConName.empty()) { + runCaptureCmd("nmcli connection delete " + + shellQuote(cellInfo.gsmConName) + " >/dev/null 2>&1"); + } + std::string cmd = "nmcli connection add type gsm con-name " + + shellQuote(cleanApn) + " ifname '*' apn " + + shellQuote(cleanApn) + " connection.autoconnect yes"; + if (!trimStr(apnUser).empty()) { + cmd += " gsm.username " + shellQuote(trimStr(apnUser)); + } + if (!trimStr(apnPass).empty()) { + cmd += " gsm.password " + shellQuote(trimStr(apnPass)); + } + cmd += " >/dev/null 2>&1 && nmcli connection up " + + shellQuote(cleanApn) + " >/dev/null 2>&1"; + int rc = 0; + runCaptureCmd(cmd, &rc); + refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); + setBanner(rc == 0 ? ("Connected APN: " + cleanApn) : ("Saved APN: " + cleanApn)); + }; + + auto executeWifiAction = [&](int act) { + modal = NetModal::None; + if (act == 0) { + if (!targetAp.security.empty() && !targetAp.isSaved && !targetAp.inUse) { + wifiPassword.clear(); + showPassword = false; + modal = NetModal::WifiPassword; + } else { + setBanner("Connecting to " + targetAp.ssid + "..."); + render(); + int rc = 0; + if (targetAp.isSaved) { + runCaptureCmd("nmcli connection up " + shellQuote(targetAp.ssid) + + " >/dev/null 2>&1", &rc); + } else { + runCaptureCmd("nmcli device wifi connect " + shellQuote(targetAp.ssid) + + " >/dev/null 2>&1", &rc); + } + refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1"); + setBanner(rc == 0 ? ("Connected: " + targetAp.ssid) : "Connection failed"); + } + } else if (act == 1) { + wifiPassword.clear(); + showPassword = false; + modal = NetModal::WifiPassword; + } else if (act == 2) { + runCaptureCmd("nmcli connection down " + shellQuote(targetAp.ssid) + + " >/dev/null 2>&1"); + refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1"); + setBanner("Disconnected " + targetAp.ssid); + } else if (act == 3) { + runCaptureCmd("nmcli connection delete " + shellQuote(targetAp.ssid) + + " >/dev/null 2>&1"); + refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1"); + setBanner("Forgot " + targetAp.ssid); + } + }; + + auto executeBtAction = [&](int act) { + modal = NetModal::None; + int rc = 0; + if (act == 0) { + setBanner("Connecting " + targetBt.name + "..."); + render(); + runCaptureCmd("bluetoothctl connect " + shellQuote(targetBt.mac) + + " >/dev/null 2>&1", &rc); + refreshCurrentTab(false); + setBanner(rc == 0 ? ("Connected: " + targetBt.name) : "Connect failed"); + } else if (act == 1) { + setBanner("Disconnecting " + targetBt.name + "..."); + render(); + runCaptureCmd("bluetoothctl disconnect " + shellQuote(targetBt.mac) + + " >/dev/null 2>&1", &rc); + refreshCurrentTab(false); + setBanner("Disconnected " + targetBt.name); + } else if (act == 2) { + setBanner("Pairing " + targetBt.name + "..."); + render(); + runCaptureCmd("bluetoothctl --timeout 8 pair " + shellQuote(targetBt.mac) + + " >/dev/null 2>&1; bluetoothctl trust " + + shellQuote(targetBt.mac) + " >/dev/null 2>&1; " + "bluetoothctl connect " + shellQuote(targetBt.mac) + + " >/dev/null 2>&1", &rc); + refreshCurrentTab(false); + setBanner("Paired & trusted " + targetBt.name); + } else if (act == 3) { + runCaptureCmd("bluetoothctl remove " + shellQuote(targetBt.mac) + + " >/dev/null 2>&1", &rc); + refreshCurrentTab(false); + setBanner("Removed " + targetBt.name); + } + }; + + auto handleEnterKey = [&]() { + if (modal == NetModal::WifiActions) { + executeWifiAction(actionSel); + return; + } + if (modal == NetModal::WifiPassword) { + connectWifiNow(targetAp.ssid, wifiPassword); + return; + } + if (modal == NetModal::ApnEditor) { + if (apnFieldSel == 3) { + saveAndConnectApn(); + } else { + apnFieldSel = (apnFieldSel + 1) % 4; + } + return; + } + if (modal == NetModal::BtActions) { + executeBtAction(actionSel); + return; + } + + if (activeTab == NetTab::Wifi) { + if (wifiRowSel == 0) { + setBanner(wifiOn ? "Turning Wi-Fi OFF..." : "Turning Wi-Fi ON..."); + render(); + runCaptureCmd("doas -n sxmo_wifitoggle.sh " + + std::string(wifiOn ? "off" : "on") + " >/dev/null 2>&1"); + refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1"); + } else if (wifiRowSel - 1 < static_cast(wifiList.size())) { + targetAp = wifiList[wifiRowSel - 1]; + if (!targetAp.inUse && !targetAp.isSaved && !targetAp.security.empty()) { + wifiPassword.clear(); + showPassword = false; + modal = NetModal::WifiPassword; + } else { + actionSel = 0; + modal = NetModal::WifiActions; + } + } + } else if (activeTab == NetTab::Cellular) { + if (cellRowSel == 0) { + if (!cellInfo.gsmConfigured) { + apnName = cellInfo.apn; + apnUser = cellInfo.username; + apnPass.clear(); + apnFieldSel = 0; + modal = NetModal::ApnEditor; + } else { + bool turnOn = !cellInfo.gsmActive; + setBanner(turnOn ? "Enabling Mobile Data..." : "Disabling Mobile Data..."); + render(); + runCaptureCmd("nmcli connection " + std::string(turnOn ? "up " : "down ") + + shellQuote(cellInfo.gsmConName) + " >/dev/null 2>&1"); + refreshCurrentTab(false); + } + } else if (cellRowSel == 1) { + apnName = cellInfo.apn; + apnUser = cellInfo.username; + apnPass.clear(); + apnFieldSel = 0; + modal = NetModal::ApnEditor; + } else if (cellRowSel == 2) { + if (cellInfo.hotspotActive) { + setBanner("Stopping Wi-Fi Hotspot..."); + render(); + runCaptureCmd("nmcli connection delete Hotspot >/dev/null 2>&1"); + refreshCurrentTab(false); + setBanner("Hotspot stopped"); + } else { + setBanner("Starting Wi-Fi Hotspot..."); + render(); + std::string hsOut = trimStr(runCaptureCmd( + "nmcli device wifi hotspot 2>&1")); + refreshCurrentTab(false); + setBanner(hsOut.empty() ? "Hotspot active" : hsOut, 4000); + } + runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1"); + } else if (cellRowSel == 3) { + if (cellInfo.usbShareActive) { + setBanner("Stopping USB Tethering..."); + render(); + runCaptureCmd("nmcli connection delete USBNetworkShare >/dev/null 2>&1"); + refreshCurrentTab(false); + setBanner("USB Tethering stopped"); + } else { + setBanner("Starting USB Tethering..."); + render(); + runCaptureCmd("nmcli connection add type ethernet ifname usb0 " + "con-name USBNetworkShare ipv4.method shared >/dev/null 2>&1 && " + "nmcli connection up USBNetworkShare >/dev/null 2>&1"); + refreshCurrentTab(false); + setBanner("USB Tethering active"); + } + } + } else if (activeTab == NetTab::Bluetooth) { + if (btRowSel == 0) { + setBanner(btOn ? "Turning Bluetooth OFF..." : "Turning Bluetooth ON..."); + render(); + runCaptureCmd("SXMO_OS=alpine doas -n sxmo_bluetoothtoggle.sh " + + std::string(btOn ? "off" : "on") + " >/dev/null 2>&1"); + refreshCurrentTab(false); + } else if (btRowSel == 1) { + setBanner("Scanning Bluetooth devices (5s)..."); + render(); + refreshCurrentTab(true); + setBanner("Found " + std::to_string(btList.size()) + " device(s)"); + } else if (btRowSel - 2 < static_cast(btList.size())) { + targetBt = btList[btRowSel - 2]; + actionSel = 0; + modal = NetModal::BtActions; + } + } + }; + + auto handleLeftSoftkey = [&]() { + if (modal == NetModal::ApnEditor) { + saveAndConnectApn(); + return; + } + if (modal != NetModal::None) { + handleEnterKey(); + return; + } + if (activeTab == NetTab::Wifi) { + setBanner("Scanning Wi-Fi networks..."); + render(); + refreshCurrentTab(true); + setBanner("Found " + std::to_string(wifiList.size()) + " network(s)"); + } else if (activeTab == NetTab::Cellular) { + refreshCurrentTab(false); + setBanner("Refreshed cellular state"); + } else if (activeTab == NetTab::Bluetooth) { + setBanner("Scanning Bluetooth (5s)..."); + render(); + refreshCurrentTab(true); + setBanner("Found " + std::to_string(btList.size()) + " device(s)"); + } + }; + + auto handleRightSoftkey = [&]() { + if (modal != NetModal::None) { + modal = NetModal::None; + return; + } + hideWindow(); + }; + + if (!isPreload) { + showWindow(openArg); + } + + int xfd = ConnectionNumber(dpy); + while (gRunning) { + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == Expose && ev.xexpose.count == 0) { + render(); + } else if (ev.type == VisibilityNotify && visible && + ev.xvisibility.state != VisibilityUnobscured) { + XRaiseWindow(dpy, win); + } else if (ev.type == KeyRelease) { + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + } else if (ev.type == KeyPress && visible) { + int64_t now = nowMs(); + if (now < startupIgnoreUntilMs) continue; + + KeySym sym = XLookupKeysym(&ev.xkey, 0); + unsigned int kc = ev.xkey.keycode; + + bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu || + sym == XF86XK_TaskPane); + bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back || + sym == XK_Escape || + sym == static_cast(XF86XK_KbdLcdMenu2)); + + if (isLSK || isRSK) { + if (!initialSoftkeysReleased) continue; + int mask = querySoftkeysDown(dpy); + if (mask == 3 || (softkeyPending != 0 && + ((isLSK && softkeyPending == 2) || + (isRSK && softkeyPending == 1)))) { + softkeyPending = 0; + writeSoftkeySuppress(now + 400); + hideWindow(); + sendPluginCommand("xdrawer", "TOGGLE"); + continue; + } + softkeyPending = isLSK ? 1 : 2; + softkeyDeadlineMs = now + 90; + continue; + } + + if (sym == XF86XK_HangupPhone || sym == XK_Cancel) { + hideWindow(); + continue; + } + + if (modal == NetModal::WifiPassword) { + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '*') { + showPassword = !showPassword; + render(); + continue; + } + if (sym == XK_Return || sym == XK_KP_Enter) { + connectWifiNow(targetAp.ssid, wifiPassword); + render(); + continue; + } + if (handleMultiTapKey(wifiPassword, sym, kc, ev.xkey.state)) { + render(); + } + continue; + } + + if (modal == NetModal::ApnEditor) { + if (sym == XK_Up || sym == XK_KP_Up) { + apnFieldSel = (apnFieldSel + 3) % 4; + lastTapDigit = -1; + render(); + continue; + } + if (sym == XK_Down || sym == XK_KP_Down) { + apnFieldSel = (apnFieldSel + 1) % 4; + lastTapDigit = -1; + render(); + continue; + } + if (sym == XK_Return || sym == XK_KP_Enter) { + handleEnterKey(); + render(); + continue; + } + if (apnFieldSel < 3) { + std::string* dst = (apnFieldSel == 0) ? &apnName + : ((apnFieldSel == 1) ? &apnUser : &apnPass); + if (handleMultiTapKey(*dst, sym, kc, ev.xkey.state)) { + render(); + } + } + continue; + } + + if (modal == NetModal::WifiActions || modal == NetModal::BtActions) { + if (sym == XK_Up || sym == XK_KP_Up) { + actionSel = (actionSel + 3) % 4; + render(); + } else if (sym == XK_Down || sym == XK_KP_Down) { + actionSel = (actionSel + 1) % 4; + render(); + } else if (sym == XK_Return || sym == XK_KP_Enter) { + handleEnterKey(); + render(); + } + continue; + } + + // Main View Tab & Row Navigation + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '1') { + activeTab = NetTab::Wifi; + refreshCurrentTab(false); + render(); + continue; + } else if (dch == '2') { + activeTab = NetTab::Cellular; + refreshCurrentTab(false); + render(); + continue; + } else if (dch == '3') { + activeTab = NetTab::Bluetooth; + refreshCurrentTab(false); + render(); + continue; + } + + if (sym == XK_Left || sym == XK_KP_Left) { + activeTab = static_cast((static_cast(activeTab) + 2) % 3); + refreshCurrentTab(false); + render(); + } else if (sym == XK_Right || sym == XK_KP_Right) { + activeTab = static_cast((static_cast(activeTab) + 1) % 3); + refreshCurrentTab(false); + render(); + } else if (sym == XK_Up || sym == XK_KP_Up) { + if (activeTab == NetTab::Wifi) { + int total = 1 + static_cast(wifiList.size()); + wifiRowSel = (wifiRowSel + total - 1) % total; + } else if (activeTab == NetTab::Cellular) { + cellRowSel = (cellRowSel + 3) % 4; + } else if (activeTab == NetTab::Bluetooth) { + int total = 2 + static_cast(btList.size()); + btRowSel = (btRowSel + total - 1) % total; + } + render(); + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (activeTab == NetTab::Wifi) { + int total = 1 + static_cast(wifiList.size()); + wifiRowSel = (wifiRowSel + 1) % total; + } else if (activeTab == NetTab::Cellular) { + cellRowSel = (cellRowSel + 1) % 4; + } else if (activeTab == NetTab::Bluetooth) { + int total = 2 + static_cast(btList.size()); + btRowSel = (btRowSel + 1) % total; + } + render(); + } else if (sym == XK_Return || sym == XK_KP_Enter) { + handleEnterKey(); + render(); + } + } + } + + int timeoutMs = -1; + int64_t now = nowMs(); + if (visible) { + if (softkeyPending != 0) { + int64_t rem = softkeyDeadlineMs - now; + if (rem <= 0) { + int pend = softkeyPending; + softkeyPending = 0; + if (querySoftkeysDown(dpy) == 3) { + writeSoftkeySuppress(now + 400); + hideWindow(); + sendPluginCommand("xdrawer", "TOGGLE"); + } else if (pend == 1) { + handleLeftSoftkey(); + render(); + } else if (pend == 2) { + handleRightSoftkey(); + render(); + } + } else { + timeoutMs = static_cast(rem); + } + } + if (!statusBanner.empty()) { + int64_t bRem = statusBannerUntilMs - now; + if (bRem <= 0) { + statusBanner.clear(); + render(); + } else if (timeoutMs < 0 || bRem < timeoutMs) { + timeoutMs = static_cast(bRem); + } + } + } + + struct pollfd pfds[2]; + pfds[0].fd = xfd; + pfds[0].events = POLLIN; + pfds[1].fd = fifoFd; + pfds[1].events = POLLIN; + + int pr = ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs); + if (pr > 0 && fifoFd >= 0 && (pfds[1].revents & POLLIN)) { + for (const auto& cmd : readFifoLines(fifoFd)) { + if (cmd == "CLOSE") { + hideWindow(); + } else if (cmd == "RELOAD_THEME") { + pt.reload(); + if (visible) render(); + } else if (cmd == "TOGGLE") { + if (visible) hideWindow(); + else showWindow(); + } else if (cmd.rfind("OPEN", 0) == 0) { + std::string arg = (cmd.size() > 5) ? trimStr(cmd.substr(5)) : ""; + showWindow(arg); + } + } + } + } + + hideWindow(); + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + XftDrawDestroy(xftDraw); + XFreePixmap(dpy, pix); + XFreeGC(dpy, gc); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} + +int pluginInit(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("networks", "--preload"); + } + return 0; +} + +void pluginCleanup(void) {} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "networks", + "Bananarchy Wi-Fi, Cellular APN, Hotspot & Bluetooth Manager (240x320)", + pluginInit, + pluginCleanup, + nullptr, + nullptr, + nullptr, + runNetworksApp +}; diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index de43f09..283930b 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -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) { diff --git a/src/games/snake.cpp b/src/games/snake.cpp index e268cea..988684f 100644 --- a/src/games/snake.cpp +++ b/src/games/snake.cpp @@ -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 +#include +#include +#include +#ifndef _EVDEVK +#define _EVDEVK(_v) (0x10081000 + (_v)) +#endif +#include #include #include #include #include +#include +#include #include #include #include -#include -#include -#include -#include #include -#include -#include 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); diff --git a/src/games/tetris.cpp b/src/games/tetris.cpp index 14aa29b..8cc8019 100644 --- a/src/games/tetris.cpp +++ b/src/games/tetris.cpp @@ -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 +#include +#include +#include +#ifndef _EVDEVK +#define _EVDEVK(_v) (0x10081000 + (_v)) +#endif +#include #include #include #include #include +#include +#include #include #include #include -#include -#include -#include -#include #include -#include -#include 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); diff --git a/src/sxmo-dmenu/dmenu.c b/src/sxmo-dmenu/dmenu.c index c0b819f..bd9cd9d 100644 --- a/src/sxmo-dmenu/dmenu.c +++ b/src/sxmo-dmenu/dmenu.c @@ -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")) { diff --git a/src/sxmo-dwm/dwm.c b/src/sxmo-dwm/dwm.c index 6bd5db7..54e5f5a 100644 --- a/src/sxmo-dwm/dwm.c +++ b/src/sxmo-dwm/dwm.c @@ -1583,7 +1583,7 @@ is_right_softkey(KeySym ks, unsigned int keycode) #include #include "dwm_plugin.h" -#define MAX_DWM_PLUGINS 16 +#define MAX_DWM_PLUGINS 24 typedef struct { void *handle; diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp index d576258..667fbf0 100644 --- a/src/xdrawer/xdrawer.cpp +++ b/src/xdrawer/xdrawer.cpp @@ -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();