diff --git a/AGENTS.md b/AGENTS.md index 2d653dd..51bde70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f - `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`), `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`. + - `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`), `ringtones_plugin.cpp` (`ringtones.so` / `xringtones`), `wallpapers_plugin.cpp` (`wallpapers.so` / `xwallpapers` / `xbackground`). + - `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcallscreen`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `xmodem`, `xnetworks`, `xbluetooth`, `xringtones`, `xwallpapers`, `xbackground`, `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`). @@ -24,7 +24,7 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f ## 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`, `modem`, `networks`). +- `./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`, `ringtones`, `wallpapers`). - `./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`). diff --git a/MANUAL.md b/MANUAL.md index 0d887d0..793776d 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -141,8 +141,8 @@ 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`, `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`: +#### 3.1.2 Preloaded DWM plugins (`DMenu`, `XDrawer`, `Dialer`, `Call Log`, `Call Screen`, `Contacts`, `SMS`, `Files`, `Camera`, `Themes`, `Modem`, `Networks`, `Ringtones`, `Wallpapers`) +All fourteen 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. @@ -153,9 +153,12 @@ All twelve core phone overlays are compiled as shared-object plugins (`/usr/lib/ - **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**, **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**: +3. **`Dialer` (`dialer.so` / `xdialer`) — Keypad Dialer with Contact, USSD & `*#06#` 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. - **Dialing & T9 Filtering**: Typing digits filters saved contacts (`~/.config/sxmo/contacts.tsv`) and recent numbers (`modemlog.tsv`) by phone number and T9 contact name in real time. Hold **`0`** (`>= 420 ms`) or press **D-Pad `Right`** / **Left softkey (`+Sign`)** to insert `+`. + - **Direct USSD Codes & `*#06#` Dual-SIM IMEI Display**: + - Typing **`*#06#`** immediately opens the **Device IMEI (`*#06#`)** modal showing both **`IMEI 1 (SIM Slot 1)`**, **`IMEI 2 (SIM Slot 2)`**, and **`IMEI SV`**. + - Typing any USSD / MMI code (`*...#` or `#...#`, e.g., `*100#`) switches the center softkey to **`SEND`**. Pressing **Call (green)** or **Center `OK` (`Return`)** sends the USSD request directly and opens an in-dialer **USSD Session Modal** with scrollable response text and an interactive reply input box for multi-step carrier menus. - **Navigation & Calling**: `Up`/`Down` selects a matching contact or recent number; **Call (green)** or **Center `OK` (`Return`)** places the call immediately; **Right softkey (`Clear` / `Close`)** or **`BackSpace`** deletes digits or closes the dialer; **Left softkey (`Call Log`)** when empty switches directly to the Call Log. 4. **`Call Log` (`calllog.so` / `xcalllog`) — Call History Viewer**: @@ -201,17 +204,18 @@ All twelve core phone overlays are compiled as shared-object plugins (`/usr/lib/ - **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`)**: +11. **`Modem, Dual-SIM & PIN/PUK Manager` (`modem.so` / `xmodem`) — Dual-SIM Slot Provisioning, PIN/PUK Entry, Radio & AT Console**: + - **Open**: Select **Modem** from the Sxmo **System menu** (`Left softkey`), or run `xmodem` (`xmodem unlock`, `xmodem info`, `xmodem at`, `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` / `4`)**: - **`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`. + - **`2:Radio` (Modem Controls & USSD)**: Displays live signal strength (`%` and visual bar), registration state, access technology (`LTE` / `UMTS` / `GSM`), operator name, both `IMEI 1` and `IMEI 2`, and phone number, plus interactive rows to switch **Network Mode** (`4G/3G/2G`, `3G/2G`, `4G Only`, `3G Only`, `2G Only`), **Send USSD Code**, toggle **Modem Power**, and **Restart ModemManager**. + - **`3:AT` (Direct AT Command Console)**: Send any AT command directly to the Qualcomm modem (`/dev/wwan0at1` / `/dev/wwan0at0`) and view the multi-line response in real time. Supports multi-tap AT command entry (`[ABC]`/`[abc]`/`[123]`/`[Sym]`, `#` cycles mode, `*` inserts `+?=,",;*#`), a quick preset selector (`AT`, `ATI`, `AT+CGSN`, `AT+CSQ`, `AT+CREG?`, `AT+COPS?`, `AT+CPIN?`, `AT+CCID`, `AT+CGDCONT?`, `AT+CGPADDR`, `AT+CFUN?`, `AT+CLCC`), and scrollable output. + - **`4: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`. @@ -220,6 +224,16 @@ All twelve core phone overlays are compiled as shared-object plugins (`/usr/lib/ - **`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**). +13. **`Ringtones & Ringer Manager` (`ringtones.so` / `xringtones`) — Ringtone Browser, Live Preview & Ringer Settings**: + - **Open**: Select **Ringtones** from the **Config**, **Audio**, or **Apps** menu, or run `xringtones`. + - **Tabs (`Left`/`Right` or `1` / `2`)**: + - **`1:Ringtones`**: Lists built-in synthesized ringtones (`/usr/share/sxmo/ringtones/*.wav` and `/usr/share/sxmo/ring.ogg`) alongside user audio files in `~/Ringtones/` and `~/Music/`. Press **`*`** or **Left softkey (`Play` / `Stop`)** to preview any ringtone live via `mpv`, and press **`Return` (`Set Ring`)** to save it as the active incoming call ringtone (`~/.config/sxmo/ringtone`). + - **`2:Settings`**: Adjust **Ringer Mode** (`Ring + Vibrate`, `Ring Only`, `Vibrate Only`, `Silent`), **Ring Duration** (`10s`–`60s`), **Ring Loop Count** (`3x`–`50x`), **Vibrate Strength** (`Level 1`–`3`), and **Test Vibration** (`~/.config/sxmo/ringsettings`). + +14. **`Desktop Wallpaper Manager` (`wallpapers.so` / `xwallpapers` / `xbackground`) — Live Preview & Wallpaper Selector**: + - **Open**: Select **Wallpapers** from the **Config** or **Apps** menu, or run `xwallpapers` / `xbackground`. + - **Controls**: Browses system wallpapers (`/usr/share/wallpapers/`) and user photos (`~/Pictures/Wallpapers/` and `~/Pictures/`) with a live decoded portrait thumbnail, image resolution, and file size. Press **`Left` / `Right`** (or `#`) to cycle the scaling mode (`Fill (Crop)`, `Scale (Stretch)`, `Fit (Max)`, `Center (1:1)`, `Tile`), press **`*`** or **Left softkey (`View [*]`)** to toggle a full-screen preview, and press **`Return` (`Apply`)** to apply the wallpaper immediately via `feh` and persist it in `~/.config/sxmo/wallpaper`. + #### 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`): diff --git a/README.md b/README.md index 1880757..242acaf 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Bananarchy targets an ultra-lightweight **Sxmo X11 (`sxmo-de-dwm`)** environment - **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. + - **`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 `+`, single-press **Call** dialing, direct USSD code execution (`*100#`, etc.) with interactive session reply modal, and instant **`*#06#`** Dual-SIM IMEI (`IMEI 1` & `IMEI 2`) display. - **`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`). - **`Contacts` (`contacts.so` / `xcontacts`)**: full-featured phonebook application opened from the idle desktop via the **Right softkey (`Names`)** or from the Sxmo menu, integrated with `~/.config/sxmo/contacts.tsv` (live T9/number search, call/SMS/dialer actions, built-in contact editor with keypad multi-tap name entry and recent-caller number picker, and contact deletion). @@ -96,7 +96,7 @@ 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, modem, networks), theme.h & runner +│ ├── dwm-plugins/ # DWM plugins (dialer, calllog, callscreen, contacts, sms, files, camera, themes, modem, networks, ringtones, wallpapers), 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 @@ -110,7 +110,7 @@ bananarchy/ │ └── 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, 11 C++ DWM plugins, and st (HarfBuzz ligatures) +│ ├── sxmo-x11/ # Prebuilt armv7 patched dwm, dmenu.so/dmenu, 13 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 diff --git a/overlay/rootfs/usr/bin/bananarchy-x11-session.sh b/overlay/rootfs/usr/bin/bananarchy-x11-session.sh index 2d0d980..270b9c3 100755 --- a/overlay/rootfs/usr/bin/bananarchy-x11-session.sh +++ b/overlay/rootfs/usr/bin/bananarchy-x11-session.sh @@ -81,7 +81,23 @@ else fi xrdb -merge "$HOME/.Xresources" >/dev/null 2>&1 || true -feh --bg-fill /usr/share/wallpapers/bananarchy.png >/dev/null 2>&1 /dev/null)" + [ -n "$custom_wp" ] && [ -f "$custom_wp" ] && WP_FILE="$custom_wp" +fi +WP_FLAG="--bg-fill" +if [ -f "$XDG_CONFIG_HOME/sxmo/wallpaper_mode" ]; then + wp_mode="$(head -n 1 "$XDG_CONFIG_HOME/sxmo/wallpaper_mode" 2>/dev/null)" + case "$wp_mode" in + scale|--bg-scale) WP_FLAG="--bg-scale" ;; + max|--bg-max) WP_FLAG="--bg-max" ;; + center|--bg-center) WP_FLAG="--bg-center" ;; + tile|--bg-tile) WP_FLAG="--bg-tile" ;; + *) WP_FLAG="--bg-fill" ;; + esac +fi +feh --no-fehbg "$WP_FLAG" "$WP_FILE" >/dev/null 2>&1 /dev/null 2>&1 || CONKY_DPY=:0 conky -c /home/user/.config/sxmo/conky.conf -d >/dev/null 2>&1 /dev/null 2>&1 || fcitx5 -d >/tmp/fcitx5.log 2>&1 /tmp/sxmo_hook_start.log 2>&1 /dev/null 2>&1; then - feh --no-fehbg --bg-fill /usr/share/wallpapers/bananarchy.png & + if command -v feh >/dev/null 2>&1; then + WP_FILE="/usr/share/wallpapers/bananarchy.png" + if [ -f "$XDG_CONFIG_HOME/sxmo/wallpaper" ]; then + custom_wp="$(head -n 1 "$XDG_CONFIG_HOME/sxmo/wallpaper" 2>/dev/null)" + [ -n "$custom_wp" ] && [ -f "$custom_wp" ] && WP_FILE="$custom_wp" + fi + WP_FLAG="--bg-fill" + if [ -f "$XDG_CONFIG_HOME/sxmo/wallpaper_mode" ]; then + wp_mode="$(head -n 1 "$XDG_CONFIG_HOME/sxmo/wallpaper_mode" 2>/dev/null)" + case "$wp_mode" in + scale|--bg-scale) WP_FLAG="--bg-scale" ;; + max|--bg-max) WP_FLAG="--bg-max" ;; + center|--bg-center) WP_FLAG="--bg-center" ;; + tile|--bg-tile) WP_FLAG="--bg-tile" ;; + *) WP_FLAG="--bg-fill" ;; + esac + fi + [ -f "$WP_FILE" ] && feh --no-fehbg "$WP_FLAG" "$WP_FILE" & fi } diff --git a/overlay/rootfs/usr/bin/xbackground b/overlay/rootfs/usr/bin/xbackground new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xbackground @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/xringtones b/overlay/rootfs/usr/bin/xringtones new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xringtones @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/xwallpapers b/overlay/rootfs/usr/bin/xwallpapers new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xwallpapers @@ -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 8a7e0e0..661947d 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 c21e833..7af3985 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 382407d..31a5cd9 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 a0019e1..799f9b4 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 a3e1464..229c41a 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 716adb7..3ae5a0b 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 73b7771..f275e07 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 index 157ff30..dbec846 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/modem.so 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 index 811d380..12706ad 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/networks.so and b/overlay/rootfs/usr/lib/dwm/plugins/networks.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so b/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so new file mode 100755 index 0000000..2154985 Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/sms.so b/overlay/rootfs/usr/lib/dwm/plugins/sms.so index 5d4e95e..17fca6a 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 0adc50e..fb24bab 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/wallpapers.so b/overlay/rootfs/usr/lib/dwm/plugins/wallpapers.so new file mode 100755 index 0000000..efd9e7f Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/wallpapers.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so index f4a4a0f..3a6e4d0 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 index 6cca8dc..a4e4037 100755 --- a/overlay/rootfs/usr/libexec/msm-modem-uim-selection +++ b/overlay/rootfs/usr/libexec/msm-modem-uim-selection @@ -408,8 +408,165 @@ case "$cmd" in exit 1 ;; + imei) + imei1="" + imei2="" + imei_sv="" + if detect_qmicli_modem; then + dms_ids="$($QMICLI_MODEM --dms-get-ids 2>/dev/null || true)" + imei1="$(printf "%s\n" "$dms_ids" | sed -n "s/.*IMEI: '\([0-9]\{14,16\}\)'/\1/p" | head -n1)" + imei_sv="$(printf "%s\n" "$dms_ids" | sed -n "s/.*IMEI SV: '\([^']*\)'/\1/p" | head -n1)" + fi + if [ -z "$imei1" ]; then + imei1="$(mmcli -m any -K 2>/dev/null | awk -F': ' '/modem\.3gpp\.imei/ {print $2; exit}')" + fi + # Query secondary AT channel (/dev/wwan0at1) if present + if [ -c "/dev/wwan0at1" ]; then + at_out="$(timeout 1 sh -c ' + stty -F /dev/wwan0at1 raw -echo 115200 2>/dev/null || true + (cat /dev/wwan0at1 & pid=$!; printf "AT+CGSN\r\n" > /dev/wwan0at1; sleep 0.25; kill $pid 2>/dev/null) 2>/dev/null + ' 2>/dev/null | tr -d "\r" | grep -oE '^[0-9]{15}$' | head -n1 || true)" + if [ -n "$at_out" ] && [ "$at_out" != "$imei1" ]; then + imei2="$at_out" + fi + fi + # Scan factory partitions for Luhn-valid 15-digit IMEIs (TAC 35...) + if [ -z "$imei2" ]; then + for part in /dev/disk/by-partlabel/traceability /dev/disk/by-partlabel/tunning /dev/disk/by-partlabel/devinfo /dev/disk/by-partlabel/oem; do + [ -e "$part" ] || continue + cand_list="$(strings -n 14 "$part" 2>/dev/null | grep -oE '35[0-9]{13}' | awk ' + { + val = $0 + sum = 0 + for (i = 1; i <= 14; i++) { + d = substr(val, i, 1) + 0 + if (i % 2 == 0) { + d *= 2 + if (d > 9) d -= 9 + } + sum += d + } + chk = (10 - (sum % 10)) % 10 + if (chk == (substr(val, 15, 1) + 0)) print val + } + ' || true)" + for c in $cand_list; do + if [ -z "$imei1" ]; then + imei1="$c" + elif [ "$c" != "$imei1" ] && [ -z "$imei2" ]; then + imei2="$c" + break 2 + fi + done + done + fi + echo "IMEI1=${imei1}" + echo "IMEI2=${imei2}" + echo "IMEI_SV=${imei_sv}" + ;; + + usb-tether) + mode="${2:-status}" + case "$mode" in + on) + sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true + nft add table ip nat 2>/dev/null || true + nft 'add chain ip nat postrouting { type nat hook postrouting priority 100 ; }' 2>/dev/null || true + nft add rule ip nat postrouting oifname "wwan*" masquerade 2>/dev/null || true + nft add rule ip nat postrouting oifname "wlan*" masquerade 2>/dev/null || true + ip addr show dev usb0 2>/dev/null | grep -q "172.16.42.1" || ip addr add 172.16.42.1/16 dev usb0 2>/dev/null || true + ip link set usb0 up 2>/dev/null || true + echo "1" > /tmp/bananarchy_usb_tether.active + ;; + off) + sysctl -w net.ipv4.ip_forward=0 >/dev/null 2>&1 || true + nft delete table ip nat 2>/dev/null || true + nmcli connection delete USBNetworkShare >/dev/null 2>&1 || true + ip addr show dev usb0 2>/dev/null | grep -q "172.16.42.1" || ip addr add 172.16.42.1/16 dev usb0 2>/dev/null || true + ip link set usb0 up 2>/dev/null || true + rm -f /tmp/bananarchy_usb_tether.active + ;; + *) + if [ -f /tmp/bananarchy_usb_tether.active ] && [ "$(sysctl -n net.ipv4.ip_forward 2>/dev/null || echo 0)" = "1" ]; then + echo "ACTIVE=1" + else + echo "ACTIVE=0" + fi + ;; + esac + ;; + + at) + port="${2:-auto}" + shift 2 2>/dev/null || true + at_cmd="$*" + if [ -z "$at_cmd" ]; then + echo "ERROR: Empty AT command" + exit 1 + fi + run_at_dev() { + dev="$1" + [ -c "$dev" ] || return 1 + out="$(timeout 2 awk -v dev="$dev" -v cmd="$at_cmd" ' + BEGIN { + sys = "stty -F " dev " raw -echo 115200 min 0 time 2 2>/dev/null" + system(sys) + printf "%s\r\n", cmd > dev + close(dev) + } + ' 2>/dev/null || true)" + return 0 + } + resp="" + for candidate in "$port" wwan0at1 wwan0at0; do + case "$candidate" in + wwan0at0|wwan0at1) + dev="/dev/$candidate" + if [ -c "$dev" ]; then + resp="$(timeout 2 sh -c ' + dev="$1" + cmd="$2" + stty -F "$dev" raw -echo 115200 min 0 time 2 2>/dev/null || true + exec 3<>"$dev" + dd bs=256 count=4 status=none <&3 >/dev/null 2>&1 || true + printf "%s\r\n" "$cmd" >&3 + lines="" + i=0 + while [ "$i" -lt 8 ]; do + chunk="$(dd bs=512 count=1 status=none <&3 2>/dev/null | tr -d "\r" || true)" + if [ -n "$chunk" ]; then + lines="${lines}${chunk}" + case "$lines" in + *OK*|*ERROR*) + break + ;; + esac + fi + i=$((i + 1)) + done + exec 3>&- + printf "%s\n" "$lines" + ' _ "$dev" "$at_cmd" 2>/dev/null | sed '/^[[:space:]]*$/d' || true)" + if [ -n "$resp" ]; then + printf "%s\n" "$resp" + exit 0 + fi + fi + ;; + esac + [ "$port" != "auto" ] && [ "$candidate" = "$port" ] && break + done + mm_out="$(mmcli -m any --command="$at_cmd" 2>&1 || true)" + if [ -n "$mm_out" ]; then + printf "%s\n" "$mm_out" | sed "s/^response: '\(.*\)'$/\1/" + exit 0 + fi + echo "ERROR: No response from modem AT channel" + exit 1 + ;; + *) - echo "Usage: $0 [boot|status|switch <1|2>|verify-pin |unblock-puk |set-pin-lock |change-pin ]" >&2 + echo "Usage: $0 [boot|status|switch <1|2>|verify-pin |unblock-puk |set-pin-lock |change-pin |imei|usb-tether |at ]" >&2 exit 1 ;; esac diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh index a5b5aea..dbc147e 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh @@ -11,78 +11,86 @@ # shellcheck source=scripts/core/sxmo_common.sh . sxmo_common.sh +if [ -f "$XDG_CONFIG_HOME/sxmo/ringsettings" ]; then + # shellcheck disable=SC1091 + . "$XDG_CONFIG_HOME/sxmo/ringsettings" +fi + +if [ -f "$XDG_CONFIG_HOME/sxmo/ringtone" ]; then + custom_ring="$(head -n 1 "$XDG_CONFIG_HOME/sxmo/ringtone" 2>/dev/null)" + if [ -n "$custom_ring" ] && [ -f "$custom_ring" ]; then + SXMO_RINGTONE="$custom_ring" + fi +fi + # Only vibrate if you already got an active call if sxmo_modemcall.sh list_active_calls \ - | grep -v ringing-in \ - | grep -q .; then - sxmo_vibrate 1500 "${SXMO_VIBRATE_STRENGTH:-1}" - exit + | grep -v ringing-in \ + | grep -q .; then + sxmo_vibrate 1500 "${SXMO_VIBRATE_STRENGTH:-1}" + exit fi # Shallow if you have more than one ringing call if ! sxmo_modemcall.sh list_active_calls \ - | grep -c ringing-in \ - | grep -q 1; then - exit + | grep -c ringing-in \ + | grep -q 1; then + exit fi finish() { - trap - INT TERM EXIT - jobs -p | xargs -r kill - wait - exit + trap - INT TERM EXIT + jobs -p | xargs -r kill + wait + exit } ring() { - mpv --no-resume-playback --quiet --no-video \ - --loop="${SXMO_RINGNUMBER:-10}" "$SXMO_RINGTONE" >/dev/null & + mpv --no-resume-playback --quiet --no-video \ + --loop="${SXMO_RINGNUMBER:-10}" "$SXMO_RINGTONE" >/dev/null & } vibrate() { - while : ; do - trap 'finish' INT TERM EXIT - sxmo_vibrate 1500 "${SXMO_VIBRATE_STRENGTH:-1}" & - wait "$!" + while : ; do + trap 'finish' INT TERM EXIT + sxmo_vibrate 1500 "${SXMO_VIBRATE_STRENGTH:-1}" & + wait "$!" - sleep 0.5 & - wait "$!" - done & + sleep 0.5 & + wait "$!" + done & } # SILENT MODE if [ -f "$XDG_CONFIG_HOME"/sxmo/.busy ]; then - end="$(cat "$XDG_CONFIG_HOME"/sxmo/.busy)" - if [ -z "$end" ] || [ "$(date +%s)" -lt "$end" ]; then - exit - fi + end="$(cat "$XDG_CONFIG_HOME"/sxmo/.busy)" + if [ -z "$end" ] || [ "$(date +%s)" -lt "$end" ]; then + exit + fi fi # RING & VIBRATE MODE (DEFAULT) if [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ] && [ ! -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ]; then - sxmo_log "RING AND VIBRATE" + sxmo_log "RING AND VIBRATE" - # In order for this to work, you will need to install playerctl and run playerctld - # In order for this to work with mpv, you will need to install mpv-mdis. - sxmo_playerctl.sh pause_all + sxmo_playerctl.sh pause_all - ring - vibrate + ring + vibrate # RING-ONLY MODE elif [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ] && [ -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ]; then - sxmo_log "RING ONLY" + sxmo_log "RING ONLY" - # In order for this to work, you will need to install playerctl and run playerctld - # In order for this to work with mpv, you will need to install mpv-mdis. - sxmo_playerctl.sh pause_all + sxmo_playerctl.sh pause_all - ring + ring # VIBRATE-ONLY MODE elif [ ! -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ] && [ -f "$XDG_CONFIG_HOME"/sxmo/.noring ]; then - smxo_log "VIBRATE ONLY" + sxmo_log "VIBRATE ONLY" - vibrate + vibrate fi trap 'finish' INT TERM EXIT diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav new file mode 100644 index 0000000..9e17490 Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav new file mode 100644 index 0000000..cdbe3d2 Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav new file mode 100644 index 0000000..b4907dc Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav new file mode 100644 index 0000000..4158aed Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav new file mode 100644 index 0000000..3ad50f9 Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav new file mode 100644 index 0000000..da8b66e Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav new file mode 100644 index 0000000..382d4e4 Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav differ diff --git a/overlay/rootfs/usr/share/wallpapers/banana-grid.png b/overlay/rootfs/usr/share/wallpapers/banana-grid.png new file mode 100644 index 0000000..eca8a9c Binary files /dev/null and b/overlay/rootfs/usr/share/wallpapers/banana-grid.png differ diff --git a/overlay/rootfs/usr/share/wallpapers/cyber-sunset.png b/overlay/rootfs/usr/share/wallpapers/cyber-sunset.png new file mode 100644 index 0000000..34a8aa7 Binary files /dev/null and b/overlay/rootfs/usr/share/wallpapers/cyber-sunset.png differ diff --git a/overlay/rootfs/usr/share/wallpapers/matrix-rain.png b/overlay/rootfs/usr/share/wallpapers/matrix-rain.png new file mode 100644 index 0000000..74cda60 Binary files /dev/null and b/overlay/rootfs/usr/share/wallpapers/matrix-rain.png differ diff --git a/overlay/rootfs/usr/share/wallpapers/nordic-peaks.png b/overlay/rootfs/usr/share/wallpapers/nordic-peaks.png new file mode 100644 index 0000000..d15405c Binary files /dev/null and b/overlay/rootfs/usr/share/wallpapers/nordic-peaks.png differ diff --git a/prebuilt/sxmo-x11/usr/bin/dwm-plugin b/prebuilt/sxmo-x11/usr/bin/dwm-plugin index a1aca55..997e3b7 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/xbackground b/prebuilt/sxmo-x11/usr/bin/xbackground new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xbackground @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/bin/xringtones b/prebuilt/sxmo-x11/usr/bin/xringtones new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xringtones @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/bin/xwallpapers b/prebuilt/sxmo-x11/usr/bin/xwallpapers new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xwallpapers @@ -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 8a7e0e0..661947d 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 c21e833..7af3985 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 382407d..31a5cd9 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 a0019e1..799f9b4 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 a3e1464..229c41a 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 716adb7..3ae5a0b 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 73b7771..f275e07 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 index 157ff30..dbec846 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so 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 index 811d380..12706ad 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so new file mode 100755 index 0000000..2154985 Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.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 5d4e95e..17fca6a 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 0adc50e..fb24bab 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/wallpapers.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so new file mode 100755 index 0000000..efd9e7f Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.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 f4a4a0f..3a6e4d0 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/build-sxmo-x11.sh b/scripts/build-sxmo-x11.sh index fd75b27..94c4510 100755 --- a/scripts/build-sxmo-x11.sh +++ b/scripts/build-sxmo-x11.sh @@ -150,6 +150,14 @@ EOF /bananarchy/src/dwm-plugins/networks_plugin.cpp $PLUG_LIBS \ -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so + $CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \ + /bananarchy/src/dwm-plugins/ringtones_plugin.cpp $PLUG_LIBS \ + -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so + + $CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \ + /bananarchy/src/dwm-plugins/wallpapers_plugin.cpp $PLUG_LIBS $MEDIA_LIBS \ + -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so + $STRIP --strip-unneeded \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so \ @@ -161,7 +169,9 @@ EOF /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/modem.so \ - /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so \ + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so \ + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so $CC $OPT_CFLAGS -std=c99 -D_DEFAULT_SOURCE -Wall -Wextra \ /bananarchy/src/dwm-plugins/dwm_plugin_runner.c -ldl \ @@ -183,6 +193,9 @@ EOF 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 + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xringtones + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xwallpapers + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xbackground 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/dialer_plugin.cpp b/src/dwm-plugins/dialer_plugin.cpp index 9050b7c..2f834c9 100644 --- a/src/dwm-plugins/dialer_plugin.cpp +++ b/src/dwm-plugins/dialer_plugin.cpp @@ -152,6 +152,19 @@ int dialer_run_main(int argc, char** argv) { bool zeroHeld = false; int64_t zeroPressMs = 0; + enum class DialerModal { + Inactive = 0, + Imei = 1, + Ussd = 2, + }; + DialerModal modal = DialerModal::Inactive; + DeviceImeis deviceImeis; + std::string ussdCode; + std::string ussdResponse; + std::string ussdReplyInput; + bool ussdSessionActive = false; + int ussdScrollLine = 0; + auto updateFilter = [&]() { filtered.clear(); if (number.empty()) { @@ -256,15 +269,23 @@ int dialer_run_main(int argc, char** argv) { subText = "Dial: " + c.number; subCol = &colYellow; } else if (!number.empty()) { - std::string exactName = book.resolveName(number); - if (!exactName.empty()) { - subText = exactName; - subCol = &colGreen; - } else if (!filtered.empty()) { - subText = filtered[0].name + " (" + filtered[0].number + ")"; - subCol = &colBlue; + if (number == "*#06#") { + subText = "Show Device IMEI (1 & 2)"; + subCol = &colYellow; + } else if (isUssdCode(number)) { + subText = "USSD Code (Press Call/OK to send)"; + subCol = &colYellow; } else { - subText = "Press Call to dial (Hold 0: +)"; + std::string exactName = book.resolveName(number); + if (!exactName.empty()) { + subText = exactName; + subCol = &colGreen; + } else if (!filtered.empty()) { + subText = filtered[0].name + " (" + filtered[0].number + ")"; + subCol = &colBlue; + } else { + subText = "Press Call to dial (Hold 0: +)"; + } } } else { subText = "0-9/*/# Dial | Hold 0: +"; @@ -339,9 +360,111 @@ int dialer_run_main(int argc, char** argv) { XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); std::string leftLabel = number.empty() ? "Call Log" : "+Sign"; - std::string centerLabel = "CALL"; + std::string centerLabel = + (selected < 0 && isUssdCode(number)) ? "SEND" : "CALL"; std::string rightLabel = number.empty() ? "Close" : "Clear"; + if (modal == DialerModal::Imei) { + XSetForeground(dpy, gc, theme.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 10, 22, kWinWidth - 20, kWinHeight - kSoftbarH - 34); + XSetForeground(dpy, gc, theme.spec.accent); + XDrawRectangle(dpy, pix, gc, 10, 22, kWinWidth - 21, kWinHeight - kSoftbarH - 35); + + drawPango(layoutIcon, &colYellow, 18, 28, 24, kIconInfo); + drawPango(layoutMain, &colYellow, 38, 28, 24, "Device IMEI (*#06#)", 184); + + XSetForeground(dpy, gc, theme.spec.divider); + XFillRectangle(dpy, pix, gc, 16, 54, kWinWidth - 32, 1); + + // Slot 1 IMEI card + XSetForeground(dpy, gc, theme.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 16, 62, kWinWidth - 32, 54); + XSetForeground(dpy, gc, theme.spec.border); + XDrawRectangle(dpy, pix, gc, 16, 62, kWinWidth - 33, 53); + drawPango(layoutSmall, &colBlue, 22, 65, 18, "IMEI 1 (SIM Slot 1):"); + drawPango(layoutMain, &colWhite, 22, 85, 24, + deviceImeis.imei1.empty() ? "Unavailable" : deviceImeis.imei1, + kWinWidth - 44); + + // Slot 2 IMEI card + XSetForeground(dpy, gc, theme.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 16, 124, kWinWidth - 32, 54); + XSetForeground(dpy, gc, theme.spec.border); + XDrawRectangle(dpy, pix, gc, 16, 124, kWinWidth - 33, 53); + drawPango(layoutSmall, &colGreen, 22, 127, 18, "IMEI 2 (SIM Slot 2):"); + drawPango(layoutMain, &colWhite, 22, 147, 24, + deviceImeis.imei2.empty() ? "Unavailable" : deviceImeis.imei2, + kWinWidth - 44); + + std::string svLine = "IMEI SV: " + + (deviceImeis.imeiSv.empty() ? "--" : deviceImeis.imeiSv); + drawPango(layoutSmall, &colMuted, 22, 188, 20, svLine, kWinWidth - 44); + + leftLabel = ""; + centerLabel = "OK"; + rightLabel = "Close"; + } else if (modal == DialerModal::Ussd) { + XSetForeground(dpy, gc, theme.spec.bgOverlay); + XFillRectangle(dpy, pix, gc, 8, 14, kWinWidth - 16, kWinHeight - kSoftbarH - 22); + XSetForeground(dpy, gc, theme.spec.accent); + XDrawRectangle(dpy, pix, gc, 8, 14, kWinWidth - 17, kWinHeight - kSoftbarH - 23); + + std::string hdr = "USSD (" + ussdCode + ")"; + drawPango(layoutMain, &colYellow, 16, 18, 22, hdr, kWinWidth - 32); + + // Response area + XSetForeground(dpy, gc, theme.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 14, 42, kWinWidth - 28, 152); + XSetForeground(dpy, gc, theme.spec.border); + XDrawRectangle(dpy, pix, gc, 14, 42, kWinWidth - 29, 151); + + pango_layout_set_width(layoutSmall, (kWinWidth - 40) * PANGO_SCALE); + pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); + pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE); + pango_layout_set_text(layoutSmall, ussdResponse.c_str(), + static_cast(ussdResponse.size())); + int lineCount = pango_layout_get_line_count(layoutSmall); + int maxVisLines = 7; + if (ussdScrollLine > std::max(0, lineCount - maxVisLines)) { + ussdScrollLine = std::max(0, lineCount - maxVisLines); + } + int curY = 46; + for (int li = ussdScrollLine; + li < lineCount && (li - ussdScrollLine) < maxVisLines; ++li) { + PangoLayoutLine* pl = pango_layout_get_line_readonly(layoutSmall, li); + if (pl) { + PangoRectangle ink{}, logical{}; + pango_layout_line_get_pixel_extents(pl, &ink, &logical); + pango_xft_render_layout_line( + xftDraw, &colWhite, pl, 20 * PANGO_SCALE, + (curY - logical.y) * PANGO_SCALE); + curY += std::max(18, logical.height + 2); + } + } + pango_layout_set_width(layoutSmall, -1); + + // Reply / Follow-up USSD input box + drawPango(layoutSmall, ussdSessionActive ? &colGreen : &colMuted, + 16, 198, 18, + ussdSessionActive ? "Session active - Reply:" : "Reply or dial USSD:"); + XSetForeground(dpy, gc, theme.spec.bgInputFocus); + XFillRectangle(dpy, pix, gc, 14, 216, kWinWidth - 28, 26); + XSetForeground(dpy, gc, theme.spec.selBg); + XDrawRectangle(dpy, pix, gc, 14, 216, kWinWidth - 29, 25); + drawPango(layoutMain, &colWhite, 20, 216, 26, ussdReplyInput + "_", + kWinWidth - 40); + + if (!ussdReplyInput.empty() || ussdSessionActive) { + leftLabel = "Send"; + centerLabel = "SEND"; + rightLabel = "Close"; + } else { + leftLabel = "Retry"; + centerLabel = "OK"; + rightLabel = "Close"; + } + } + drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel); int cw = pangoWidth(layoutMain, centerLabel); drawPango(layoutMain, &colGreen, (kWinWidth - cw) / 2, sbY + 1, @@ -367,8 +490,31 @@ int dialer_run_main(int argc, char** argv) { render(); XSync(dpy, False); + auto openImeiModal = [&]() { + deviceImeis = queryDeviceImeis(); + modal = DialerModal::Imei; + number.clear(); + selected = allContacts.empty() ? -1 : 0; + updateFilter(); + render(); + }; + + auto executeUssd = [&](const std::string& codeOrReply, bool isReply) { + modal = DialerModal::Ussd; + if (!isReply || isUssdCode(codeOrReply)) { + ussdCode = codeOrReply; + } + ussdScrollLine = 0; + ussdResponse = "Sending USSD " + codeOrReply + "..."; + ussdReplyInput.clear(); + render(); + ussdResponse = sendModemUssd(codeOrReply, isReply, &ussdSessionActive); + render(); + }; + auto showWindow = [&](const std::string& arg) { closeOtherPlugins("dialer"); + modal = DialerModal::Inactive; bool themeChanged = theme.checkAndReload( dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, layoutNum, win); @@ -394,7 +540,9 @@ int dialer_run_main(int argc, char** argv) { usleep(2000); } bool reloaded = reloadContactsIfNeeded(); - if (!cleanArg.empty() || reloaded || themeChanged || number != cleanArg) { + if (cleanArg == "*#06#") { + openImeiModal(); + } else if (!cleanArg.empty() || reloaded || themeChanged || number != cleanArg) { number = cleanArg; selected = number.empty() ? (allContacts.empty() ? -1 : 0) : -1; updateFilter(); @@ -408,8 +556,12 @@ int dialer_run_main(int argc, char** argv) { } number += cleanArg; selected = -1; - updateFilter(); - render(); + if (number == "*#06#") { + openImeiModal(); + } else { + updateFilter(); + render(); + } } }; @@ -440,7 +592,13 @@ int dialer_run_main(int argc, char** argv) { softkeyPending = 0; softkeyDeadlineMs = 0; zeroHeld = false; - if (!number.empty() || selected != (allContacts.empty() ? -1 : 0)) { + if (modal == DialerModal::Ussd && ussdSessionActive) { + cancelModemUssd(); + ussdSessionActive = false; + } + if (modal != DialerModal::Inactive || !number.empty() || + selected != (allContacts.empty() ? -1 : 0)) { + modal = DialerModal::Inactive; number.clear(); selected = allContacts.empty() ? -1 : 0; updateFilter(); @@ -557,6 +715,79 @@ int dialer_run_main(int argc, char** argv) { } char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state); + + if (modal == DialerModal::Imei) { + if (sym == XK_Return || sym == XK_KP_Enter || sym == XK_Escape || + sym == XK_BackSpace || sym == XF86XK_Phone || + sym == XF86XK_PickupPhone) { + modal = DialerModal::Inactive; + render(); + } + continue; + } + + if (modal == DialerModal::Ussd) { + if (dialCh != '\0') { + if (ussdReplyInput.size() < 32) { + ussdReplyInput.push_back(dialCh); + render(); + } + continue; + } + if (sym == XK_BackSpace || sym == XK_Left || sym == XK_KP_Left) { + if (!ussdReplyInput.empty()) { + ussdReplyInput.pop_back(); + render(); + } else if (sym == XK_BackSpace) { + if (ussdSessionActive) { + cancelModemUssd(); + ussdSessionActive = false; + } + modal = DialerModal::Inactive; + render(); + } + continue; + } + if (sym == XK_Escape) { + if (ussdSessionActive) { + cancelModemUssd(); + ussdSessionActive = false; + } + modal = DialerModal::Inactive; + render(); + continue; + } + if (sym == XK_Up || sym == XK_KP_Up) { + if (ussdScrollLine > 0) { + --ussdScrollLine; + render(); + } + continue; + } + if (sym == XK_Down || sym == XK_KP_Down) { + ++ussdScrollLine; + render(); + continue; + } + if (sym == XK_Return || sym == XK_KP_Enter || + sym == XF86XK_Phone || sym == XF86XK_PickupPhone || + sym == XF86XK_Send) { + if (!ussdReplyInput.empty()) { + if (ussdReplyInput == "*#06#") { + openImeiModal(); + } else { + std::string req = ussdReplyInput; + executeUssd(req, !isUssdCode(req)); + } + } else if (!ussdSessionActive) { + modal = DialerModal::Inactive; + render(); + } + continue; + } + continue; + } + if (dialCh != '\0') { if (nowMs() - openStartMs < 40 && number.size() == 1 && number[0] == dialCh) { @@ -570,6 +801,10 @@ int dialer_run_main(int argc, char** argv) { } else { zeroHeld = false; } + if (number == "*#06#") { + openImeiModal(); + continue; + } updateFilter(); render(); continue; @@ -632,12 +867,22 @@ int dialer_run_main(int argc, char** argv) { case XK_Return: case XK_KP_Enter: case XF86XK_Phone: - case XF86XK_PickupPhone: { + case XF86XK_PickupPhone: + case XF86XK_Send: { std::string target = selectActiveNumber(); if (!target.empty()) { - writeSoftkeySuppress(nowMs() + 350); - hideWindow(true); - placePhoneCall(target); + if (target == "*#06#") { + openImeiModal(); + } else if (isUssdCode(target)) { + number.clear(); + selected = allContacts.empty() ? -1 : 0; + updateFilter(); + executeUssd(target, false); + } else { + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + placePhoneCall(target); + } } break; } @@ -658,7 +903,21 @@ int dialer_run_main(int argc, char** argv) { runAsyncArgv("xdrawer"); } } else if (comb == 1) { - if (number.empty()) { + if (modal == DialerModal::Imei) { + modal = DialerModal::Inactive; + render(); + } else if (modal == DialerModal::Ussd) { + if (!ussdReplyInput.empty()) { + if (ussdReplyInput == "*#06#") { + openImeiModal(); + } else { + std::string req = ussdReplyInput; + executeUssd(req, !isUssdCode(req)); + } + } else if (!ussdCode.empty()) { + executeUssd(ussdCode, false); + } + } else if (number.empty()) { writeSoftkeySuppress(nowMs() + 250); hideWindow(false); if (!sendPluginCommand("calllog", "OPEN")) { @@ -675,7 +934,17 @@ int dialer_run_main(int argc, char** argv) { render(); } } else if (comb == 2) { - if (!number.empty()) { + if (modal == DialerModal::Imei) { + modal = DialerModal::Inactive; + render(); + } else if (modal == DialerModal::Ussd) { + if (ussdSessionActive) { + cancelModemUssd(); + ussdSessionActive = false; + } + modal = DialerModal::Inactive; + render(); + } else if (!number.empty()) { number.pop_back(); selected = -1; updateFilter(); diff --git a/src/dwm-plugins/dwm_plugin_runner.c b/src/dwm-plugins/dwm_plugin_runner.c index 435362b..e3cfe7f 100644 --- a/src/dwm-plugins/dwm_plugin_runner.c +++ b/src/dwm-plugins/dwm_plugin_runner.c @@ -274,6 +274,13 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) { strcasecmp(base, "xbluetooth") == 0 || strcasecmp(base, "bluetooth") == 0) { return "networks"; } + if (strcasecmp(base, "xringtones") == 0 || strcasecmp(base, "ringtones") == 0) { + return "ringtones"; + } + if (strcasecmp(base, "xwallpapers") == 0 || strcasecmp(base, "wallpapers") == 0 || + strcasecmp(base, "xbackground") == 0 || strcasecmp(base, "background") == 0) { + return "wallpapers"; + } return NULL; } diff --git a/src/dwm-plugins/modem_plugin.cpp b/src/dwm-plugins/modem_plugin.cpp index a8d9944..e576921 100644 --- a/src/dwm-plugins/modem_plugin.cpp +++ b/src/dwm-plugins/modem_plugin.cpp @@ -29,7 +29,8 @@ void handleSignal(int) { enum class TabId { SimCards = 0, RadioInfo = 1, - ModemLog = 2, + AtConsole = 2, + ModemLog = 3, }; enum class ModalMode { @@ -94,6 +95,8 @@ struct ModemStatusData { std::string accessTech; int signalQuality = 0; std::string imei; + std::string imei2; + std::string imeiSv; std::string ownNumber; std::string simDbusPath; std::string simIccid; @@ -126,18 +129,42 @@ struct ModemLogRow { 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; -} +constexpr const char* kAtPresets[] = { + "AT", + "ATI", + "AT+CGSN", + "AT+CSQ", + "AT+CREG?", + "AT+COPS?", + "AT+CPIN?", + "AT+CCID", + "AT+CGDCONT?", + "AT+CGPADDR", + "AT+CFUN?", + "AT+CLCC", +}; +constexpr int kNumAtPresets = + static_cast(sizeof(kAtPresets) / sizeof(kAtPresets[0])); + +enum class AtTextMode { + Upper = 0, // ABC + Lower = 1, // abc + Num = 2, // 123 + Sym = 3, // Sym +}; + +const char* kAtTapMap[10] = { + " 0+=", + "+?=,1\"':;/-_", + "ABC2", + "DEF3", + "GHI4", + "JKL5", + "MNO6", + "PQRS7", + "TUV8", + "WXYZ9" +}; void parseKvLines(const std::string& text, std::unordered_map& out, @@ -231,6 +258,11 @@ ModemStatusData queryModemAndSimStatus() { if (d.imei.empty()) { d.imei = cleanMmVal(mmKv["modem.generic.equipment-identifier"]); } + DeviceImeis devImeis = queryDeviceImeis(); + if (!devImeis.imei1.empty()) d.imei = devImeis.imei1; + if (!devImeis.imei2.empty()) d.imei2 = devImeis.imei2; + if (!devImeis.imeiSv.empty()) d.imeiSv = devImeis.imeiSv; + if (d.imei2.empty()) d.imei2 = d.imei; 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"]); @@ -452,6 +484,18 @@ int runModemApp(int argc, char** argv) { std::string ussdResponse; bool ussdWaitingReply = false; + // AT Console state + int atFocusRow = 0; // 0 = Input, 1 = Preset/Port, 2 = Output scroll + int atPresetIdx = 0; + int atPortIdx = 0; // 0 = auto, 1 = wwan0at0, 2 = wwan0at1 + std::string atInput = "AT"; + std::string atOutput = "Enter an AT command and press Send.\nUse Preset row (< >) for common commands."; + int atScrollLine = 0; + AtTextMode atTextMode = AtTextMode::Upper; + int atLastTapKey = -1; + int atLastTapIdx = 0; + int64_t atLastTapMs = 0; + std::string statusBanner; int64_t statusBannerUntilMs = 0; @@ -517,6 +561,8 @@ int runModemApp(int argc, char** argv) { activeTab = TabId::ModemLog; book.load(); logRows = loadRecentModemLog(book); + } else if (a == "at" || a == "atcmd") { + activeTab = TabId::AtConsole; } else if (a == "ussd") { activeTab = TabId::RadioInfo; modal = ModalMode::UssdDialog; @@ -604,9 +650,9 @@ int runModemApp(int argc, char** argv) { 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) { + const char* tabLabels[4] = {"1:SIMs", "2:Radio", "3:AT", "4:Log"}; + int tabW = kWinWidth / 4; + for (int t = 0; t < 4; ++t) { bool isSel = (static_cast(activeTab) == t); if (isSel) { XSetForeground(dpy, gc, pt.spec.selBg); @@ -742,12 +788,11 @@ int runModemApp(int argc, char** argv) { (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 imei1Line = "IMEI 1: " + (status.imei.empty() ? "--" : status.imei); + drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imei1Line, 220); - std::string iccidLine = "ICCID: " + - (status.simIccid.empty() ? "--" : status.simIccid); - drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, iccidLine, 220); + std::string imei2Line = "IMEI 2: " + (status.imei2.empty() ? "--" : status.imei2); + drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, imei2Line, 220); // 5 Interactive Radio Control Rows std::string modeLabel = kRadioModes[status.modePresetIdx].label; @@ -772,6 +817,75 @@ int runModemApp(int argc, char** argv) { } drawSoftkeyBar("Refresh", "Apply", "Close"); + } else if (activeTab == TabId::AtConsole) { + const char* modeBadge = "[ABC]"; + if (atTextMode == AtTextMode::Lower) modeBadge = "[abc]"; + else if (atTextMode == AtTextMode::Num) modeBadge = "[123]"; + else if (atTextMode == AtTextMode::Sym) modeBadge = "[Sym]"; + + const char* portNames[3] = {"auto", "wwan0at0", "wwan0at1"}; + const char* portLabels[3] = {"Auto", "SIM1 (at0)", "SIM2 (at1)"}; + (void)portNames; + + // Row 0: AT Command Input Box (Y=contentY..contentY+46) + drawPango(layoutSmall, &colYellow, 8, contentY, 18, + std::string(kIconTerm) + " AT Command (# mode, * +?=):"); + int mbW = pangoTextWidth(layoutSmall, modeBadge); + drawPango(layoutSmall, &colGreen, kWinWidth - mbW - 8, contentY, 18, modeBadge); + + bool focus0 = (atFocusRow == 0); + XSetForeground(dpy, gc, focus0 ? pt.spec.bgInputFocus : pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 12, 26); + XSetForeground(dpy, gc, focus0 ? pt.spec.accent : pt.spec.border); + XDrawRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 13, 25); + drawPango(layoutMain, &colWhite, 12, contentY + 19, 26, + atInput + (focus0 ? "_" : ""), kWinWidth - 24); + + // Row 1: Preset & AT Port Selector (Y=contentY+49..contentY+73) + bool focus1 = (atFocusRow == 1); + XSetForeground(dpy, gc, focus1 ? pt.spec.selBg : pt.spec.bgAltOdd); + XFillRectangle(dpy, pix, gc, 6, contentY + 48, kWinWidth - 12, 24); + std::string presetRow = "< Preset: " + std::string(kAtPresets[atPresetIdx]) + + " | *Port: " + portLabels[atPortIdx] + " >"; + drawPango(layoutSmall, focus1 ? &colWhite : &colLight, + 10, contentY + 48, 24, presetRow, kWinWidth - 20); + + // Row 2: AT Response Output Area (Y=contentY+75..contentY+contentH-2) + bool focus2 = (atFocusRow == 2); + int outY = contentY + 75; + int outH = contentH - 77; + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 6, outY, kWinWidth - 12, outH); + XSetForeground(dpy, gc, focus2 ? pt.spec.accent : pt.spec.border); + XDrawRectangle(dpy, pix, gc, 6, outY, kWinWidth - 13, outH - 1); + + pango_layout_set_width(layoutSmall, (kWinWidth - 24) * PANGO_SCALE); + pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR); + pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE); + pango_layout_set_text(layoutSmall, atOutput.c_str(), + static_cast(atOutput.size())); + int lineCount = pango_layout_get_line_count(layoutSmall); + int maxVisLines = std::max(1, (outH - 8) / 17); + if (atScrollLine > std::max(0, lineCount - maxVisLines)) { + atScrollLine = std::max(0, lineCount - maxVisLines); + } + int curY = outY + 4; + for (int li = atScrollLine; + li < lineCount && (li - atScrollLine) < maxVisLines; ++li) { + PangoLayoutLine* pl = pango_layout_get_line_readonly(layoutSmall, li); + if (pl) { + PangoRectangle ink{}, logical{}; + pango_layout_line_get_pixel_extents(pl, &ink, &logical); + pango_xft_render_layout_line( + xftDraw, &colLight, pl, 12 * PANGO_SCALE, + (curY - logical.y) * PANGO_SCALE); + curY += std::max(16, logical.height + 1); + } + } + pango_layout_set_width(layoutSmall, -1); + + drawSoftkeyBar("Send", "Send", + (atFocusRow == 0 && !atInput.empty()) ? "Clear" : "Close"); } else if (activeTab == TabId::ModemLog) { if (logRows.empty()) { drawPango(layoutMain, &colMuted, 16, contentY + 60, 24, @@ -1144,16 +1258,17 @@ int runModemApp(int argc, char** argv) { } } else if (modal == ModalMode::UssdDialog) { if (ussdInput.empty()) return; - ussdResponse = "Sending USSD request..."; + if (ussdInput == "*#06#") { + DeviceImeis im = queryDeviceImeis(); + ussdResponse = "IMEI 1: " + (im.imei1.empty() ? "--" : im.imei1) + + "\nIMEI 2: " + (im.imei2.empty() ? "--" : im.imei2) + + "\nIMEI SV: " + (im.imeiSv.empty() ? "--" : im.imeiSv); + ussdWaitingReply = false; + return; + } + ussdResponse = "Sending USSD " + ussdInput + "..."; 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); + ussdResponse = sendModemUssd(ussdInput, ussdWaitingReply, &ussdWaitingReply); if (ussdWaitingReply) { ussdInput.clear(); } @@ -1212,6 +1327,30 @@ int runModemApp(int argc, char** argv) { } }; + auto executeAtCommand = [&]() { + std::string cmd = trimStr(atInput); + if (cmd.empty()) { + setBanner("AT command cannot be empty"); + return; + } + const char* portArg = (atPortIdx == 1) + ? "/dev/wwan0at0" + : (atPortIdx == 2 ? "/dev/wwan0at1" : "auto"); + atOutput = "Sending: " + cmd + " ..."; + atScrollLine = 0; + render(); + std::string out = trimStr(runCaptureCmd( + "doas -n /usr/libexec/msm-modem-uim-selection at " + + shellQuote(portArg) + " " + shellQuote(cmd) + " 2>&1")); + if (out.empty()) { + atOutput = "> " + cmd + "\n(No response)"; + } else { + atOutput = "> " + cmd + "\n" + out; + } + atScrollLine = 0; + setBanner("AT response received"); + }; + auto handleLeftSoftkey = [&]() { if (modal == ModalMode::SimActions) { int act = actionSel; @@ -1256,6 +1395,8 @@ int runModemApp(int argc, char** argv) { } else if (activeTab == TabId::RadioInfo) { refreshData(false); setBanner("Modem status refreshed"); + } else if (activeTab == TabId::AtConsole) { + executeAtCommand(); } else if (activeTab == TabId::ModemLog) { refreshData(true); setBanner("Modem log refreshed"); @@ -1312,6 +1453,12 @@ int runModemApp(int argc, char** argv) { refreshData(false); setBanner("Refreshed"); } + } else if (activeTab == TabId::AtConsole) { + if (atFocusRow == 1) { + atPortIdx = (atPortIdx + 1) % 3; + } else { + executeAtCommand(); + } } else if (activeTab == TabId::ModemLog) { if (!logRows.empty() && logRowSel >= 0 && logRowSel < static_cast(logRows.size())) { @@ -1474,8 +1621,84 @@ int runModemApp(int argc, char** argv) { continue; } - // Main View Navigation + // AT Console text input when atFocusRow == 0 char dch = mapDialKey(sym, kc, ev.xkey.state); + if (activeTab == TabId::AtConsole && atFocusRow == 0) { + if (sym == XK_BackSpace || sym == XK_Delete) { + if (!atInput.empty()) { + atInput.pop_back(); + atLastTapKey = -1; + render(); + } + continue; + } + if (dch == '#') { + atTextMode = static_cast( + (static_cast(atTextMode) + 1) % 4); + atLastTapKey = -1; + render(); + continue; + } + if (dch == '*') { + const char* syms = "+?=,1\"':;/-_*#"; + int len = static_cast(std::strlen(syms)); + if (atLastTapKey == 10 && (now - atLastTapMs) < 850 && + !atInput.empty()) { + atLastTapIdx = (atLastTapIdx + 1) % len; + atInput.back() = syms[atLastTapIdx]; + } else if (atInput.size() < 64) { + atLastTapKey = 10; + atLastTapIdx = 0; + atInput.push_back(syms[0]); + } + atLastTapMs = now; + render(); + continue; + } + if (dch >= '0' && dch <= '9') { + int digit = dch - '0'; + if (atTextMode == AtTextMode::Num) { + if (atInput.size() < 64) { + atInput.push_back(dch); + atLastTapKey = -1; + } + } else if (atTextMode == AtTextMode::Sym) { + const char* symTable = "+?=,.\"':;/-_"; + char sc = symTable[digit % 10]; + if (atInput.size() < 64) { + atInput.push_back(sc); + atLastTapKey = -1; + } + } else { + const char* seq = kAtTapMap[digit]; + int len = static_cast(std::strlen(seq)); + if (atLastTapKey == digit && (now - atLastTapMs) < 850 && + !atInput.empty()) { + atLastTapIdx = (atLastTapIdx + 1) % len; + char chOut = seq[atLastTapIdx]; + if (atTextMode == AtTextMode::Lower) { + chOut = static_cast( + std::tolower(static_cast(chOut))); + } + atInput.back() = chOut; + } else if (atInput.size() < 64) { + atLastTapKey = digit; + atLastTapIdx = 0; + char chOut = seq[0]; + if (atTextMode == AtTextMode::Lower) { + chOut = static_cast( + std::tolower(static_cast(chOut))); + } + atInput.push_back(chOut); + } + atLastTapMs = now; + } + render(); + continue; + } + } + + // Main View Navigation (4 tabs: 1:SIMs, 2:Radio, 3:AT, 4:Log) if (dch == '1') { activeTab = TabId::SimCards; render(); @@ -1485,25 +1708,39 @@ int runModemApp(int argc, char** argv) { render(); continue; } else if (dch == '3') { + activeTab = TabId::AtConsole; + render(); + continue; + } else if (dch == '4') { activeTab = TabId::ModemLog; refreshData(true); render(); continue; + } else if (activeTab == TabId::AtConsole && dch == '*') { + atPortIdx = (atPortIdx + 1) % 3; + render(); + continue; } if (sym == XK_Left || sym == XK_KP_Left) { if (activeTab == TabId::RadioInfo && radioRowSel == 0) { applyRadioModePreset(status.modePresetIdx - 1); + } else if (activeTab == TabId::AtConsole && atFocusRow == 1) { + atPresetIdx = (atPresetIdx + kNumAtPresets - 1) % kNumAtPresets; + atInput = kAtPresets[atPresetIdx]; } else { - activeTab = static_cast((static_cast(activeTab) + 2) % 3); + activeTab = static_cast((static_cast(activeTab) + 3) % 4); 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 if (activeTab == TabId::AtConsole && atFocusRow == 1) { + atPresetIdx = (atPresetIdx + 1) % kNumAtPresets; + atInput = kAtPresets[atPresetIdx]; } else { - activeTab = static_cast((static_cast(activeTab) + 1) % 3); + activeTab = static_cast((static_cast(activeTab) + 1) % 4); if (activeTab == TabId::ModemLog) refreshData(true); } render(); @@ -1512,6 +1749,12 @@ int runModemApp(int argc, char** argv) { simRowSel = (simRowSel + 5) % 6; } else if (activeTab == TabId::RadioInfo) { radioRowSel = (radioRowSel + 4) % 5; + } else if (activeTab == TabId::AtConsole) { + if (atFocusRow == 2 && atScrollLine > 0) { + --atScrollLine; + } else { + atFocusRow = (atFocusRow + 2) % 3; + } } else if (activeTab == TabId::ModemLog && !logRows.empty()) { logRowSel = std::max(0, logRowSel - 1); } @@ -1521,11 +1764,19 @@ int runModemApp(int argc, char** argv) { simRowSel = (simRowSel + 1) % 6; } else if (activeTab == TabId::RadioInfo) { radioRowSel = (radioRowSel + 1) % 5; + } else if (activeTab == TabId::AtConsole) { + if (atFocusRow == 2) { + ++atScrollLine; + } else { + atFocusRow = (atFocusRow + 1) % 3; + } } 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) { + } else if (sym == XK_Return || sym == XK_KP_Enter || + sym == XF86XK_Send || sym == XF86XK_Phone || + sym == XF86XK_PickupPhone) { handleEnterKey(); render(); } @@ -1565,7 +1816,8 @@ int runModemApp(int argc, char** argv) { } } // Periodic 5-second status refresh while visible on SIM/Radio tabs - if (modal == ModalMode::None && activeTab != TabId::ModemLog) { + if (modal == ModalMode::None && activeTab != TabId::ModemLog && + activeTab != TabId::AtConsole) { int64_t rRem = (lastRefreshMs + 5000) - now; if (rRem <= 0) { refreshData(false); diff --git a/src/dwm-plugins/networks_plugin.cpp b/src/dwm-plugins/networks_plugin.cpp index 2cbf112..0e46aaa 100644 --- a/src/dwm-plugins/networks_plugin.cpp +++ b/src/dwm-plugins/networks_plugin.cpp @@ -82,19 +82,6 @@ void popUtf8Char(std::string& s) { 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; @@ -254,6 +241,12 @@ CellularNetInfo queryCellularInfo() { info.usbShareActive = true; } } + if (!info.usbShareActive) { + struct stat st {}; + if (::stat("/tmp/bananarchy_usb_tether.active", &st) == 0) { + info.usbShareActive = true; + } + } if (info.gsmConfigured && !info.gsmConName.empty()) { std::string det = runCaptureCmd( @@ -433,7 +426,6 @@ int runNetworksApp(int argc, char** argv) { auto& colGreen = pt.colGreen; auto& colBlue = pt.colBlue; auto& colOrange = pt.colOrange; - auto& colRed = pt.colRed; NetTab activeTab = NetTab::Wifi; NetModal modal = NetModal::None; @@ -750,7 +742,7 @@ int runNetworksApp(int argc, char** argv) { drawPango(layoutSmall, isSel ? &colWhite : &colLight, 10, ry, 30, rows[r], kWinWidth - 20); } - drawSoftkeyBar("Refresh", "Toggle/Edit", "Close"); + drawSoftkeyBar("Refresh", "Select", "Close"); } else if (activeTab == NetTab::Bluetooth) { // Row 0: BT Power Toggle (height 28) bool sel0 = (btRowSel == 0); @@ -1174,15 +1166,13 @@ int runNetworksApp(int argc, char** argv) { if (cellInfo.usbShareActive) { setBanner("Stopping USB Tethering..."); render(); - runCaptureCmd("nmcli connection delete USBNetworkShare >/dev/null 2>&1"); + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection usb-tether off >/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"); + runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection usb-tether on >/dev/null 2>&1"); refreshCurrentTab(false); setBanner("USB Tethering active"); } diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index 283930b..bd7859e 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -21,8 +21,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -88,7 +90,8 @@ constexpr const char* kIconSliders = "\xef\x87\x9e"; // U+F1DE sliders / con 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 +constexpr const char* kIconSim = "\xf3\xb0\x99\x95"; // U+F0655 nf-md-sim +constexpr const char* kIconBell = "\xef\x83\xb3"; // U+F0F3 bell / ringtone inline int64_t nowMs() { struct timespec ts {}; @@ -344,6 +347,19 @@ inline void runAsyncCmd(const std::string& cmd) { } } +inline 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; +} + inline std::string runCaptureCmd(const std::string& cmd, int* exitCodeOut = nullptr) { FILE* fp = ::popen(cmd.c_str(), "r"); if (!fp) { @@ -1046,6 +1062,8 @@ inline void closeOtherPlugins(const char* selfPluginName) { {"xthemes.pid", "themes"}, {"xmodem.pid", "modem"}, {"xnetworks.pid", "networks"}, + {"xringtones.pid", "ringtones"}, + {"xwallpapers.pid", "wallpapers"}, }; for (const auto& p : kAllPlugins) { if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) { @@ -1074,6 +1092,8 @@ inline void broadcastThemeReload(const char* selfPluginName = nullptr) { "themes", "modem", "networks", + "ringtones", + "wallpapers", }; for (const char* name : kPlugins) { if (selfPluginName && std::strcmp(name, selfPluginName) == 0) { @@ -1355,6 +1375,260 @@ inline std::vector loadSmsThreads(const ContactBook& book) { return threads; } +struct DeviceImeis { + std::string imei1; + std::string imei2; + std::string imeiSv; +}; + +inline bool queryQrtrDmsImeis(DeviceImeis& out) { + constexpr int kAfQipcrtr = 42; + constexpr uint32_t kQrtrPortCtrl = 0xfffffffeu; + constexpr uint32_t kQrtrTypeNewServer = 4; + constexpr uint32_t kQrtrTypeNewLookup = 10; + constexpr uint32_t kQmiServiceDms = 2; + + struct SockAddrQrtr { + unsigned short sq_family; + uint32_t sq_node; + uint32_t sq_port; + }; + + struct QrtrCtrlPkt { + uint32_t cmd; + uint32_t service; + uint32_t instance; + uint32_t node; + uint32_t port; + } __attribute__((packed)); + + int fd = ::socket(kAfQipcrtr, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (fd < 0) return false; + + SockAddrQrtr localSq {}; + socklen_t sl = sizeof(localSq); + if (::getsockname(fd, reinterpret_cast(&localSq), &sl) < 0) { + ::close(fd); + return false; + } + + SockAddrQrtr ctrlSq {}; + ctrlSq.sq_family = kAfQipcrtr; + ctrlSq.sq_node = localSq.sq_node; + ctrlSq.sq_port = kQrtrPortCtrl; + + QrtrCtrlPkt lookup {}; + lookup.cmd = kQrtrTypeNewLookup; + lookup.service = kQmiServiceDms; + lookup.instance = 0; + + if (::sendto(fd, &lookup, sizeof(lookup), 0, + reinterpret_cast(&ctrlSq), + sizeof(ctrlSq)) < 0) { + ::close(fd); + return false; + } + + uint32_t dmsNode = 0; + uint32_t dmsPort = 0; + bool foundDms = false; + + for (int i = 0; i < 8; ++i) { + struct pollfd pfd { fd, POLLIN, 0 }; + if (::poll(&pfd, 1, 60) <= 0) break; + QrtrCtrlPkt pkt {}; + ssize_t n = ::recv(fd, &pkt, sizeof(pkt), 0); + if (n >= static_cast(sizeof(pkt)) && + pkt.cmd == kQrtrTypeNewServer) { + if (pkt.service == kQmiServiceDms && pkt.port != 0) { + dmsNode = pkt.node; + dmsPort = pkt.port; + foundDms = true; + break; + } + if (pkt.service == 0 && pkt.node == 0 && pkt.port == 0) { + break; + } + } + } + ::close(fd); + if (!foundDms) return false; + + auto querySubImei = [&](uint32_t subType, std::string& imeiOut, + std::string& svOut) { + int sfd = ::socket(kAfQipcrtr, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sfd < 0) return; + + SockAddrQrtr dmsSq {}; + dmsSq.sq_family = kAfQipcrtr; + dmsSq.sq_node = dmsNode; + dmsSq.sq_port = dmsPort; + + // 1. QMI_DMS_BIND_SUBSCRIPTION (0x0057), TLV 0x01 = uint32_t(subType) + if (subType > 0) { + uint8_t bindReq[14] = { + 0x00, 0x01, 0x00, // flags=0 (req), txn_id=1 + 0x57, 0x00, // msg_id = 0x0057 + 0x07, 0x00, // tlv_len = 7 + 0x01, 0x04, 0x00, // TLV 0x01, len 4 + static_cast(subType & 0xFF), 0x00, 0x00, 0x00 + }; + ::sendto(sfd, bindReq, sizeof(bindReq), 0, + reinterpret_cast(&dmsSq), + sizeof(dmsSq)); + struct pollfd pfd { sfd, POLLIN, 0 }; + if (::poll(&pfd, 1, 60) > 0) { + uint8_t discard[256]; + ::recv(sfd, discard, sizeof(discard), 0); + } + } + + // 2. QMI_DMS_GET_DEVICE_SERIAL_NUMBERS (0x0025) + uint8_t getIdsReq[7] = { + 0x00, 0x02, 0x00, // flags=0 (req), txn_id=2 + 0x25, 0x00, // msg_id = 0x0025 + 0x00, 0x00 // tlv_len = 0 + }; + if (::sendto(sfd, getIdsReq, sizeof(getIdsReq), 0, + reinterpret_cast(&dmsSq), + sizeof(dmsSq)) >= 0) { + struct pollfd pfd { sfd, POLLIN, 0 }; + if (::poll(&pfd, 1, 80) > 0) { + uint8_t buf[512]; + ssize_t rlen = ::recv(sfd, buf, sizeof(buf), 0); + if (rlen >= 7 && buf[0] == 0x02 && + buf[3] == 0x25 && buf[4] == 0x00) { + size_t tlvLen = static_cast(buf[5]) | + (static_cast(buf[6]) << 8); + size_t pos = 7; + size_t end = std::min(static_cast(rlen), 7 + tlvLen); + while (pos + 3 <= end) { + uint8_t tType = buf[pos]; + size_t tLen = static_cast(buf[pos + 1]) | + (static_cast(buf[pos + 2]) << 8); + pos += 3; + if (pos + tLen > end) break; + if (tType == 0x11 && tLen > 0) { + imeiOut.assign(reinterpret_cast(buf + pos), tLen); + imeiOut = trimStr(imeiOut); + } else if (tType == 0x13 && tLen > 0 && svOut.empty()) { + svOut.assign(reinterpret_cast(buf + pos), tLen); + svOut = trimStr(svOut); + } + pos += tLen; + } + } + } + } + ::close(sfd); + }; + + querySubImei(1, out.imei1, out.imeiSv); + if (out.imei1.empty()) { + querySubImei(0, out.imei1, out.imeiSv); + } + querySubImei(2, out.imei2, out.imeiSv); + return !out.imei1.empty() || !out.imei2.empty(); +} + +inline DeviceImeis queryDeviceImeis() { + static DeviceImeis cached; + if (!cached.imei1.empty() && !cached.imei2.empty()) { + return cached; + } + + DeviceImeis res = cached; + queryQrtrDmsImeis(res); + + if (res.imei1.empty() || res.imei2.empty()) { + std::string helperOut = runCaptureCmd( + "doas -n /usr/libexec/msm-modem-uim-selection imei 2>/dev/null"); + std::istringstream ss(helperOut); + std::string line; + while (std::getline(ss, line)) { + if (line.rfind("IMEI1=", 0) == 0 && res.imei1.empty()) { + res.imei1 = trimStr(line.substr(6)); + } else if (line.rfind("IMEI2=", 0) == 0 && res.imei2.empty()) { + res.imei2 = trimStr(line.substr(6)); + } else if (line.rfind("IMEI_SV=", 0) == 0 && res.imeiSv.empty()) { + res.imeiSv = trimStr(line.substr(8)); + } + } + } + + if (res.imei2.empty() && !res.imei1.empty()) { + res.imei2 = res.imei1; + } + if (!res.imei1.empty()) { + cached = res; + } + return res; +} + +inline bool isUssdCode(const std::string& code) { + std::string s = trimStr(code); + return s.size() >= 3 && (s.front() == '*' || s.front() == '#') && + s.back() == '#'; +} + +inline std::string sendModemUssd(const std::string& input, + bool isReply = false, + bool* outSessionActive = nullptr) { + if (outSessionActive) *outSessionActive = false; + std::string clean = trimStr(input); + if (clean.empty()) return "Empty USSD request."; + + int rc = 0; + std::string cmd; + if (isReply && !isUssdCode(clean)) { + cmd = "mmcli -m any --3gpp-ussd-respond=" + shellQuote(clean) + " 2>&1"; + } else { + cmd = "mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1; " + "mmcli -m any --3gpp-ussd-initiate=" + shellQuote(clean) + " 2>&1"; + } + + std::string out = trimStr(runCaptureCmd(cmd, &rc)); + size_t q1 = out.find('\''); + size_t q2 = out.rfind('\''); + if (q1 != std::string::npos && q2 != std::string::npos && q2 > q1) { + out = out.substr(q1 + 1, q2 - q1 - 1); + } + if (out.empty()) { + out = (rc == 0) ? "USSD request completed." : "USSD request failed."; + } + + if (outSessionActive) { + std::string stOut = runCaptureCmd( + "mmcli -m any -K 2>/dev/null | grep 'modem.3gpp.ussd.state'"); + if (stOut.find("user-response") != std::string::npos) { + *outSessionActive = true; + } + } + + // Log USSD event to modemlog.tsv + std::string logPath = getModemLogFilePath(); + ContactBook::ensureParentDir(logPath); + std::ofstream logOut(logPath, std::ios::app); + if (logOut.is_open()) { + std::time_t now = std::time(nullptr); + struct tm tmNow {}; + localtime_r(&now, &tmNow); + char tbuf[32]; + std::strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmNow); + std::string oneLine = out; + for (char& c : oneLine) { + if (c == '\n' || c == '\r' || c == '\t') c = ' '; + } + logOut << tbuf << "\tussd\t" << clean << "\t" << oneLine << "\n"; + } + + return out; +} + +inline void cancelModemUssd() { + runAsyncCmd("mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1"); +} + } // namespace bananarchy #endif // DWM_PLUGINS_PHONE_DATA_H diff --git a/src/dwm-plugins/ringtones_plugin.cpp b/src/dwm-plugins/ringtones_plugin.cpp new file mode 100644 index 0000000..35dbfcf --- /dev/null +++ b/src/dwm-plugins/ringtones_plugin.cpp @@ -0,0 +1,858 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Bananarchy Ringtone & Ringer Manager Plugin (ringtones.so / xringtones) + +#include "phone_data.h" +#include "../sxmo-dwm/dwm_plugin.h" + +#include +#include +#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 { + Ringtones = 0, + Settings = 1, +}; + +struct RingtoneEntry { + std::string path; + std::string displayName; + std::string formatTag; + std::string sourceTag; + off_t sizeBytes = 0; +}; + +struct RingSettings { + int ringerMode = 0; // 0 = Ring + Vibrate, 1 = Ring Only, 2 = Vibrate Only, 3 = Silent + int ringTimeSec = 20; + int ringLoops = 10; + int vibrateStrength = 1; +}; + +constexpr int kRingTimeSteps[] = {10, 15, 20, 25, 30, 45, 60}; +constexpr int kNumRingTimeSteps = + static_cast(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0])); + +constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50}; +constexpr int kNumRingLoopSteps = + static_cast(sizeof(kRingLoopSteps) / sizeof(kRingLoopSteps[0])); + +std::string getConfigDir() { + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + if (xdg && *xdg) { + return std::string(xdg) + "/sxmo"; + } + return getHomeDirPath() + "/.config/sxmo"; +} + +bool isAudioExtension(const std::string& name, std::string* extUpperOut = nullptr) { + size_t dot = name.rfind('.'); + if (dot == std::string::npos || dot + 1 >= name.size()) return false; + std::string ext = toLowerStr(name.substr(dot + 1)); + if (ext == "wav" || ext == "ogg" || ext == "mp3" || + ext == "flac" || ext == "opus" || ext == "m4a") { + if (extUpperOut) { + *extUpperOut = ext; + for (char& c : *extUpperOut) { + c = static_cast(std::toupper(static_cast(c))); + } + } + return true; + } + return false; +} + +std::string prettifyFilename(const std::string& filename) { + size_t dot = filename.rfind('.'); + std::string stem = (dot == std::string::npos) ? filename : filename.substr(0, dot); + for (char& c : stem) { + if (c == '_' || c == '-') c = ' '; + } + return trimStr(stem); +} + +void scanDirectoryForRingtones(const std::string& dirPath, + const std::string& sourceTag, + std::vector& out) { + DIR* d = ::opendir(dirPath.c_str()); + if (!d) return; + std::vector names; + while (struct dirent* ent = ::readdir(d)) { + if (ent->d_name[0] == '.') continue; + names.emplace_back(ent->d_name); + } + ::closedir(d); + std::sort(names.begin(), names.end()); + + for (const auto& name : names) { + std::string extTag; + if (!isAudioExtension(name, &extTag)) continue; + std::string fullPath = dirPath + "/" + name; + struct stat st {}; + if (::stat(fullPath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) continue; + RingtoneEntry e; + e.path = fullPath; + e.displayName = prettifyFilename(name); + e.formatTag = extTag; + e.sourceTag = sourceTag; + e.sizeBytes = st.st_size; + out.push_back(e); + } +} + +std::vector discoverRingtones() { + std::vector list; + scanDirectoryForRingtones("/usr/share/sxmo/ringtones", "System", list); + + struct stat st {}; + if (::stat("/usr/share/sxmo/ring.ogg", &st) == 0 && S_ISREG(st.st_mode)) { + RingtoneEntry defEntry; + defEntry.path = "/usr/share/sxmo/ring.ogg"; + defEntry.displayName = "Sxmo Default Ring"; + defEntry.formatTag = "OGG"; + defEntry.sourceTag = "System"; + defEntry.sizeBytes = st.st_size; + list.push_back(defEntry); + } + + std::string home = getHomeDirPath(); + scanDirectoryForRingtones(home + "/Ringtones", "User", list); + scanDirectoryForRingtones(home + "/Music/Ringtones", "User", list); + scanDirectoryForRingtones(home + "/Music", "Music", list); + return list; +} + +std::string loadActiveRingtonePath() { + std::string cfgPath = getConfigDir() + "/ringtone"; + std::ifstream ifs(cfgPath); + if (ifs) { + std::string line; + if (std::getline(ifs, line)) { + line = trimStr(line); + struct stat st {}; + if (!line.empty() && ::stat(line.c_str(), &st) == 0) { + return line; + } + } + } + const char* envRing = std::getenv("SXMO_RINGTONE"); + if (envRing && *envRing) return envRing; + return "/usr/share/sxmo/ring.ogg"; +} + +bool saveActiveRingtonePath(const std::string& path) { + std::string cfgDir = getConfigDir(); + ::mkdir(cfgDir.c_str(), 0755); + std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc); + if (!ofs) return false; + ofs << path << "\n"; + return true; +} + +RingSettings loadRingSettings() { + RingSettings s; + std::string cfgDir = getConfigDir(); + bool noRing = (::access((cfgDir + "/.noring").c_str(), F_OK) == 0); + bool noVib = (::access((cfgDir + "/.novibrate").c_str(), F_OK) == 0); + if (!noRing && !noVib) s.ringerMode = 0; + else if (!noRing && noVib) s.ringerMode = 1; + else if (noRing && !noVib) s.ringerMode = 2; + else s.ringerMode = 3; + + std::ifstream ifs(cfgDir + "/ringsettings"); + std::string line; + while (std::getline(ifs, line)) { + line = trimStr(line); + if (line.empty() || line[0] == '#') continue; + auto eq = line.find('='); + if (eq == std::string::npos) continue; + std::string k = trimStr(line.substr(0, eq)); + std::string v = trimStr(line.substr(eq + 1)); + if (v.size() >= 2 && ((v.front() == '"' && v.back() == '"') || + (v.front() == '\'' && v.back() == '\''))) { + v = v.substr(1, v.size() - 2); + } + int val = std::atoi(v.c_str()); + if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) { + s.ringTimeSec = val; + } else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) { + s.ringLoops = val; + } else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) { + s.vibrateStrength = val; + } + } + return s; +} + +void saveRingSettings(const RingSettings& s) { + std::string cfgDir = getConfigDir(); + ::mkdir(cfgDir.c_str(), 0755); + + std::string noRingPath = cfgDir + "/.noring"; + std::string noVibPath = cfgDir + "/.novibrate"; + if (s.ringerMode == 0) { + ::unlink(noRingPath.c_str()); + ::unlink(noVibPath.c_str()); + } else if (s.ringerMode == 1) { + ::unlink(noRingPath.c_str()); + writeIntFile(noVibPath, 1); + } else if (s.ringerMode == 2) { + writeIntFile(noRingPath, 1); + ::unlink(noVibPath.c_str()); + } else { + writeIntFile(noRingPath, 1); + writeIntFile(noVibPath, 1); + } + + std::ofstream ofs(cfgDir + "/ringsettings", std::ios::trunc); + if (ofs) { + ofs << "SXMO_RINGTIME=" << s.ringTimeSec << "\n"; + ofs << "SXMO_RINGNUMBER=" << s.ringLoops << "\n"; + ofs << "SXMO_VIBRATE_STRENGTH=" << s.vibrateStrength << "\n"; + } + runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1"); +} + +std::string formatFileSize(off_t bytes) { + if (bytes < 1024) return std::to_string(bytes) + " B"; + return std::to_string((bytes + 512) / 1024) + " KB"; +} + +int runRingtonesApp(int argc, char** argv) { + gRunning = 1; + std::setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + + bool isPreload = false; + for (int i = 1; i < argc; ++i) { + if (argv[i] && std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } + } + + std::string daemonPidPath = getPluginDaemonPidPath("ringtones"); + std::string activePidPath = getRuntimeFile("xringtones.pid"); + + if (isPluginDaemonAlive("ringtones")) { + if (!isPreload) { + sendPluginCommand("ringtones", "OPEN"); + } + return 0; + } + + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("ringtones"); + + 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[] = "xringtones"; + char resClass[] = "XRingtones"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Ringtones"); + + 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& colWhite = pt.colWhite; + auto& colLight = pt.colLight; + auto& colMuted = pt.colMuted; + auto& colYellow = pt.colYellow; + auto& colGreen = pt.colGreen; + auto& colBlue = pt.colBlue; + + TabId activeTab = TabId::Ringtones; + std::vector tones; + std::string activePath; + RingSettings settings; + int selIdx = 0; + int settingsRow = 0; + pid_t previewPid = -1; + std::string previewPath; + + std::string statusBanner; + int64_t statusBannerUntilMs = 0; + + bool visible = false; + int64_t startupIgnoreUntilMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + auto setBanner = [&](const std::string& msg, int durationMs = 2200) { + statusBanner = msg; + statusBannerUntilMs = nowMs() + durationMs; + }; + + auto stopPreview = [&]() { + if (previewPid > 0) { + ::kill(previewPid, SIGTERM); + ::waitpid(previewPid, nullptr, WNOHANG); + previewPid = -1; + } + previewPath.clear(); + }; + + auto checkPreviewAlive = [&]() { + if (previewPid > 0) { + int st = 0; + pid_t r = ::waitpid(previewPid, &st, WNOHANG); + if (r == previewPid || (r < 0 && errno == ECHILD)) { + previewPid = -1; + previewPath.clear(); + return true; + } + } + return false; + }; + + auto togglePreview = [&](const std::string& path) { + checkPreviewAlive(); + if (previewPid > 0 && previewPath == path) { + stopPreview(); + setBanner("Preview stopped"); + return; + } + stopPreview(); + pid_t pid = ::fork(); + if (pid == 0) { + int devNull = ::open("/dev/null", O_RDWR); + if (devNull >= 0) { + ::dup2(devNull, STDIN_FILENO); + ::dup2(devNull, STDOUT_FILENO); + ::dup2(devNull, STDERR_FILENO); + if (devNull > 2) ::close(devNull); + } + ::execlp("mpv", "mpv", "--no-resume-playback", "--quiet", + "--no-video", "--loop=2", path.c_str(), nullptr); + ::_exit(127); + } else if (pid > 0) { + previewPid = pid; + previewPath = path; + setBanner("Playing preview..."); + } + }; + + auto refreshData = [&]() { + tones = discoverRingtones(); + activePath = loadActiveRingtonePath(); + settings = loadRingSettings(); + if (selIdx >= static_cast(tones.size())) { + selIdx = std::max(0, static_cast(tones.size()) - 1); + } + }; + + auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, + int h, const std::string& text, int maxW = -1, + int align = 0) { + if (text.empty()) return; + pango_layout_set_text(layout, text.c_str(), -1); + 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); + } + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + int tx = x; + if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2; + else if (align == 2 && maxW > 0) tx = x + maxW - tw; + int ty = y + (h - th) / 2; + pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE, + ty * PANGO_SCALE); + }; + + auto drawSoftkeyBar = [&](const std::string& leftLabel, + const std::string& midLabel, + const std::string& rightLabel) { + int barY = kWinHeight - 24; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24); + XSetForeground(dpy, gc, pt.spec.border); + XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); + + drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0); + drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1); + drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2); + }; + + auto render = [&]() { + if (!visible) return; + checkPreviewAlive(); + + XSetForeground(dpy, gc, pt.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // Top tab header (0..26) + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26); + + const char* tabNames[2] = {"1:Ringtones", "2:Settings"}; + for (int i = 0; i < 2; ++i) { + int tx = i * 120; + bool isAct = (static_cast(activeTab) == i); + if (isAct) { + XSetForeground(dpy, gc, pt.spec.selBg); + XFillRectangle(dpy, pix, gc, tx, 0, 120, 26); + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, tx, 24, 120, 2); + } + drawPango(layoutSmall, isAct ? &colWhite : &colMuted, tx + 4, 0, 24, + tabNames[i], 112, 1); + } + + if (activeTab == TabId::Ringtones) { + // Active ringtone summary banner (y=28..50) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 22); + std::string actName = prettifyFilename( + activePath.substr(activePath.rfind('/') + 1)); + drawPango(layoutSmall, &colGreen, 8, 28, 22, + std::string(kIconBell) + " Active: " + actName, + kWinWidth - 16); + + // List area (y=52..246, 5 rows of 38px) + constexpr int kVisibleRows = 5; + int total = static_cast(tones.size()); + int startIdx = 0; + if (total > kVisibleRows) { + startIdx = std::clamp(selIdx - kVisibleRows / 2, 0, + total - kVisibleRows); + } + + for (int i = 0; i < kVisibleRows; ++i) { + int idx = startIdx + i; + if (idx >= total) break; + const auto& e = tones[idx]; + int ry = 54 + i * 38; + bool isSel = (idx == selIdx); + bool isActive = (e.path == activePath); + bool isPlaying = (previewPid > 0 && previewPath == e.path); + + XSetForeground(dpy, gc, isSel ? pt.spec.selBg + : (i % 2 == 0 ? pt.spec.bgAltEven + : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 36); + if (isActive) { + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, 4, ry, 3, 36); + } + + drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 1, 19, + e.displayName, 152); + + std::string badge; + XftColor* badgeCol = &colMuted; + if (isPlaying) { + badge = "[PLAYING]"; + badgeCol = &colYellow; + } else if (isActive) { + badge = "[ACTIVE]"; + badgeCol = &colGreen; + } + if (!badge.empty()) { + drawPango(layoutSmall, badgeCol, 162, ry + 2, 18, badge, 70, 2); + } + + std::string sub = e.formatTag + " | " + formatFileSize(e.sizeBytes) + + " | " + e.sourceTag + " ([*] Preview)"; + drawPango(layoutSmall, isSel ? &colLight : &colMuted, 10, ry + 19, 16, + sub, kWinWidth - 20); + } + + bool curPlaying = (!tones.empty() && selIdx >= 0 && + selIdx < static_cast(tones.size()) && + previewPid > 0 && + previewPath == tones[selIdx].path); + drawSoftkeyBar(curPlaying ? "Stop" : "Play [*]", "Set Ring", "Close"); + } else { + // Tab 2: Settings + const char* modeLabels[4] = { + "Ring + Vibrate", + "Ring Only", + "Vibrate Only", + "Silent", + }; + std::string rows[5][2] = { + {"Ringer Mode", + "< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"}, + {"Ring Duration", + "< " + std::to_string(settings.ringTimeSec) + " sec >"}, + {"Ring Loop Count", + "< " + std::to_string(settings.ringLoops) + " loops >"}, + {"Vibrate Strength", + "< Level " + std::to_string(settings.vibrateStrength) + " >"}, + {"Test Vibration", "Press OK to vibrate"}, + }; + + for (int i = 0; i < 5; ++i) { + int ry = 32 + i * 42; + bool isSel = (settingsRow == i); + XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 38); + if (isSel) { + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 37); + } + drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 2, 17, + rows[i][0], kWinWidth - 20); + drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 18, 19, + rows[i][1], kWinWidth - 20); + } + drawSoftkeyBar("Change", "Select", "Close"); + } + + if (!statusBanner.empty()) { + int by = kWinHeight - 46; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19); + drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner, + kWinWidth - 20, 1); + } + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + auto showWindow = [&]() { + closeOtherPlugins("ringtones"); + refreshData(); + for (size_t i = 0; i < tones.size(); ++i) { + if (tones[i].path == activePath) { + selIdx = static_cast(i); + break; + } + } + writeIntFile(activePidPath, static_cast(::getpid())); + visible = true; + startupIgnoreUntilMs = nowMs() + 180; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + XMapRaised(dpy, win); + XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); + render(); + }; + + auto hideWindow = [&]() { + stopPreview(); + if (!visible) return; + visible = false; + softkeyPending = 0; + XUnmapWindow(dpy, win); + ::unlink(activePidPath.c_str()); + XFlush(dpy); + }; + + auto adjustSettingRow = [&](int delta) { + if (settingsRow == 0) { + settings.ringerMode = (settings.ringerMode + 4 + delta) % 4; + saveRingSettings(settings); + setBanner("Ringer mode updated"); + } else if (settingsRow == 1) { + int curIdx = 2; + for (int i = 0; i < kNumRingTimeSteps; ++i) { + if (kRingTimeSteps[i] == settings.ringTimeSec) { + curIdx = i; + break; + } + } + curIdx = (curIdx + kNumRingTimeSteps + delta) % kNumRingTimeSteps; + settings.ringTimeSec = kRingTimeSteps[curIdx]; + saveRingSettings(settings); + } else if (settingsRow == 2) { + int curIdx = 2; + for (int i = 0; i < kNumRingLoopSteps; ++i) { + if (kRingLoopSteps[i] == settings.ringLoops) { + curIdx = i; + break; + } + } + curIdx = (curIdx + kNumRingLoopSteps + delta) % kNumRingLoopSteps; + settings.ringLoops = kRingLoopSteps[curIdx]; + saveRingSettings(settings); + } else if (settingsRow == 3) { + settings.vibrateStrength = + ((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1; + saveRingSettings(settings); + } else if (settingsRow == 4) { + runAsyncCmd("sxmo_vibrate 600 " + + std::to_string(settings.vibrateStrength) + + " >/dev/null 2>&1"); + setBanner("Testing vibration..."); + } + }; + + if (!isPreload) { + showWindow(); + } + + 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; + } + + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '1') { + activeTab = TabId::Ringtones; + render(); + continue; + } else if (dch == '2') { + activeTab = TabId::Settings; + render(); + continue; + } else if (dch == '*' && activeTab == TabId::Ringtones && + !tones.empty()) { + togglePreview(tones[selIdx].path); + render(); + continue; + } + + if (sym == XK_Left || sym == XK_KP_Left) { + if (activeTab == TabId::Settings && settingsRow < 4) { + adjustSettingRow(-1); + } else { + activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings + : TabId::Ringtones; + } + render(); + } else if (sym == XK_Right || sym == XK_KP_Right) { + if (activeTab == TabId::Settings && settingsRow < 4) { + adjustSettingRow(1); + } else { + activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings + : TabId::Ringtones; + } + render(); + } else if (sym == XK_Up || sym == XK_KP_Up) { + if (activeTab == TabId::Ringtones && !tones.empty()) { + selIdx = (selIdx + static_cast(tones.size()) - 1) % + static_cast(tones.size()); + } else if (activeTab == TabId::Settings) { + settingsRow = (settingsRow + 4) % 5; + } + render(); + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (activeTab == TabId::Ringtones && !tones.empty()) { + selIdx = (selIdx + 1) % static_cast(tones.size()); + } else if (activeTab == TabId::Settings) { + settingsRow = (settingsRow + 1) % 5; + } + render(); + } else if (sym == XK_Return || sym == XK_KP_Enter) { + if (activeTab == TabId::Ringtones && !tones.empty()) { + activePath = tones[selIdx].path; + saveActiveRingtonePath(activePath); + setBanner("Ringtone set: " + tones[selIdx].displayName); + } else if (activeTab == TabId::Settings) { + adjustSettingRow(1); + } + render(); + } + } + } + + int timeoutMs = -1; + int64_t now = nowMs(); + if (visible) { + if (checkPreviewAlive()) { + render(); + } + 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) { + if (activeTab == TabId::Ringtones && !tones.empty()) { + togglePreview(tones[selIdx].path); + } else if (activeTab == TabId::Settings) { + adjustSettingRow(1); + } + render(); + } else if (pend == 2) { + hideWindow(); + } + } 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); + } + } + if (previewPid > 0 && (timeoutMs < 0 || timeoutMs > 400)) { + timeoutMs = 400; + } + } + + 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) { + showWindow(); + } + } + } + } + + hideWindow(); + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + if (xftDraw) XftDrawDestroy(xftDraw); + XFreeGC(dpy, gc); + XFreePixmap(dpy, pix); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} + +int pluginInit(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("ringtones", "--preload"); + } + return 0; +} + +void pluginCleanup(void) {} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "ringtones", + "Bananarchy Ringtones & Ringer Manager (240x320)", + pluginInit, + pluginCleanup, + nullptr, + nullptr, + nullptr, + runRingtonesApp +}; diff --git a/src/dwm-plugins/wallpapers_plugin.cpp b/src/dwm-plugins/wallpapers_plugin.cpp new file mode 100644 index 0000000..c8836b8 --- /dev/null +++ b/src/dwm-plugins/wallpapers_plugin.cpp @@ -0,0 +1,716 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Bananarchy Desktop Background / Wallpaper Manager Plugin (wallpapers.so / xwallpapers) + +#include "phone_data.h" +#include "media_utils.h" +#include "../sxmo-dwm/dwm_plugin.h" + +#include +#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; +} + +struct WallpaperEntry { + std::string path; + std::string displayName; + std::string sourceTag; + off_t sizeBytes = 0; +}; + +struct ScaleModeSpec { + const char* id; + const char* label; + const char* fehFlag; +}; + +constexpr ScaleModeSpec kScaleModes[] = { + {"fill", "Fill (Crop)", "--bg-fill"}, + {"scale", "Scale (Stretch)", "--bg-scale"}, + {"max", "Fit (Max)", "--bg-max"}, + {"center", "Center (1:1)", "--bg-center"}, + {"tile", "Tile", "--bg-tile"}, +}; +constexpr int kNumScaleModes = + static_cast(sizeof(kScaleModes) / sizeof(kScaleModes[0])); + +std::string getConfigDir() { + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + if (xdg && *xdg) { + return std::string(xdg) + "/sxmo"; + } + return getHomeDirPath() + "/.config/sxmo"; +} + +bool isWallpaperExt(const std::string& name) { + size_t dot = name.rfind('.'); + if (dot == std::string::npos || dot + 1 >= name.size()) return false; + std::string ext = toLowerStr(name.substr(dot + 1)); + return ext == "png" || ext == "jpg" || ext == "jpeg"; +} + +std::string prettifyWallpaperName(const std::string& filename) { + size_t dot = filename.rfind('.'); + std::string stem = (dot == std::string::npos) ? filename : filename.substr(0, dot); + for (char& c : stem) { + if (c == '_' || c == '-') c = ' '; + } + if (!stem.empty()) { + stem[0] = static_cast(std::toupper(static_cast(stem[0]))); + } + return trimStr(stem); +} + +void scanWallpaperDir(const std::string& dirPath, + const std::string& sourceTag, + std::vector& out) { + DIR* d = ::opendir(dirPath.c_str()); + if (!d) return; + std::vector names; + while (struct dirent* ent = ::readdir(d)) { + if (ent->d_name[0] == '.') continue; + names.emplace_back(ent->d_name); + } + ::closedir(d); + std::sort(names.begin(), names.end()); + + for (const auto& name : names) { + if (!isWallpaperExt(name)) continue; + std::string fullPath = dirPath + "/" + name; + struct stat st {}; + if (::stat(fullPath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) continue; + WallpaperEntry e; + e.path = fullPath; + e.displayName = prettifyWallpaperName(name); + e.sourceTag = sourceTag; + e.sizeBytes = st.st_size; + out.push_back(e); + } +} + +std::vector discoverWallpapers() { + std::vector list; + scanWallpaperDir("/usr/share/wallpapers", "System", list); + + std::string home = getHomeDirPath(); + scanWallpaperDir(home + "/Pictures/Wallpapers", "Wallpapers", list); + scanWallpaperDir(home + "/Pictures", "Pictures", list); + return list; +} + +std::string loadActiveWallpaperPath() { + std::ifstream ifs(getConfigDir() + "/wallpaper"); + if (ifs) { + std::string line; + if (std::getline(ifs, line)) { + line = trimStr(line); + struct stat st {}; + if (!line.empty() && ::stat(line.c_str(), &st) == 0) { + return line; + } + } + } + return "/usr/share/wallpapers/bananarchy.png"; +} + +int loadActiveScaleModeIdx() { + std::ifstream ifs(getConfigDir() + "/wallpaper_mode"); + if (ifs) { + std::string line; + if (std::getline(ifs, line)) { + line = toLowerStr(trimStr(line)); + for (int i = 0; i < kNumScaleModes; ++i) { + if (line == kScaleModes[i].id || line == kScaleModes[i].fehFlag) { + return i; + } + } + } + } + return 0; +} + +void applyAndSaveWallpaper(const std::string& path, int modeIdx) { + modeIdx = std::clamp(modeIdx, 0, kNumScaleModes - 1); + std::string cfgDir = getConfigDir(); + ::mkdir(cfgDir.c_str(), 0755); + { + std::ofstream ofs(cfgDir + "/wallpaper", std::ios::trunc); + if (ofs) ofs << path << "\n"; + } + { + std::ofstream ofs(cfgDir + "/wallpaper_mode", std::ios::trunc); + if (ofs) ofs << kScaleModes[modeIdx].id << "\n"; + } + runAsyncCmd(std::string("feh --no-fehbg ") + kScaleModes[modeIdx].fehFlag + + " " + shellQuote(path) + " >/dev/null 2>&1"); +} + +int runWallpapersApp(int argc, char** argv) { + gRunning = 1; + std::setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + + bool isPreload = false; + for (int i = 1; i < argc; ++i) { + if (argv[i] && std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } + } + + std::string daemonPidPath = getPluginDaemonPidPath("wallpapers"); + std::string activePidPath = getRuntimeFile("xwallpapers.pid"); + + if (isPluginDaemonAlive("wallpapers")) { + if (!isPreload) { + sendPluginCommand("wallpapers", "OPEN"); + } + return 0; + } + + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("wallpapers"); + + 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[] = "xwallpapers"; + char resClass[] = "XWallpapers"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Wallpapers"); + + 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& colWhite = pt.colWhite; + auto& colLight = pt.colLight; + auto& colMuted = pt.colMuted; + auto& colYellow = pt.colYellow; + auto& colGreen = pt.colGreen; + auto& colBlue = pt.colBlue; + + std::vector items; + std::string activePath; + int scaleModeIdx = 0; + int selIdx = 0; + bool fullscreenPreview = false; + + // Cached decoded image for currently selected wallpaper + std::string cachedImgPath; + std::vector cachedRgb; + int cachedImgW = 0; + int cachedImgH = 0; + bool cachedImgValid = false; + + constexpr int kThumbW = 76; + constexpr int kThumbH = 100; + std::vector thumbPixels(kThumbW * kThumbH, 0x111111); + XImage* thumbXImg = XCreateImage( + dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0, + reinterpret_cast(thumbPixels.data()), kThumbW, kThumbH, 32, 0); + + constexpr int kFullW = 240; + constexpr int kFullH = 246; + std::vector fullPixels(kFullW * kFullH, 0x111111); + XImage* fullXImg = XCreateImage( + dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0, + reinterpret_cast(fullPixels.data()), kFullW, kFullH, 32, 0); + + std::string statusBanner; + int64_t statusBannerUntilMs = 0; + + bool visible = false; + int64_t startupIgnoreUntilMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + auto setBanner = [&](const std::string& msg, int durationMs = 2200) { + statusBanner = msg; + statusBannerUntilMs = nowMs() + durationMs; + }; + + auto updatePreviewCache = [&]() { + if (items.empty() || selIdx < 0 || + selIdx >= static_cast(items.size())) { + cachedImgPath.clear(); + cachedImgValid = false; + return; + } + const std::string& p = items[selIdx].path; + if (p == cachedImgPath) return; + cachedImgPath = p; + cachedRgb.clear(); + cachedImgW = 0; + cachedImgH = 0; + cachedImgValid = loadImageRgb24(p, &cachedRgb, &cachedImgW, &cachedImgH); + if (cachedImgValid) { + scaleRgb24ToBgrxCanvas(cachedRgb.data(), cachedImgW, cachedImgH, + thumbPixels.data(), kThumbW, kThumbH, + pt.spec.bgHeader); + scaleRgb24ToBgrxCanvas(cachedRgb.data(), cachedImgW, cachedImgH, + fullPixels.data(), kFullW, kFullH, + pt.spec.bgMain); + } else { + std::fill(thumbPixels.begin(), thumbPixels.end(), pt.spec.bgHeader); + std::fill(fullPixels.begin(), fullPixels.end(), pt.spec.bgMain); + } + }; + + auto refreshData = [&]() { + items = discoverWallpapers(); + activePath = loadActiveWallpaperPath(); + scaleModeIdx = loadActiveScaleModeIdx(); + if (selIdx >= static_cast(items.size())) { + selIdx = std::max(0, static_cast(items.size()) - 1); + } + cachedImgPath.clear(); + updatePreviewCache(); + }; + + auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, + int h, const std::string& text, int maxW = -1, + int align = 0) { + if (text.empty()) return; + pango_layout_set_text(layout, text.c_str(), -1); + 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); + } + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + int tx = x; + if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2; + else if (align == 2 && maxW > 0) tx = x + maxW - tw; + int ty = y + (h - th) / 2; + pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE, + ty * PANGO_SCALE); + }; + + auto drawSoftkeyBar = [&](const std::string& leftLabel, + const std::string& midLabel, + const std::string& rightLabel) { + int barY = kWinHeight - 24; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24); + XSetForeground(dpy, gc, pt.spec.border); + XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); + + drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0); + drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1); + drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2); + }; + + auto render = [&]() { + if (!visible) return; + updatePreviewCache(); + + XSetForeground(dpy, gc, pt.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // Top Header (0..26) + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26); + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, 0, 24, kWinWidth, 2); + + std::string countTag = items.empty() + ? "0/0" + : (std::to_string(selIdx + 1) + "/" + + std::to_string(items.size())); + drawPango(layoutMain, &colWhite, 8, 0, 24, + std::string(kIconImage) + " Wallpapers", 170); + drawPango(layoutSmall, &colYellow, 180, 0, 24, countTag, 52, 2); + + if (fullscreenPreview && cachedImgValid && fullXImg) { + XPutImage(dpy, pix, gc, fullXImg, 0, 0, 0, 26, kFullW, kFullH); + drawSoftkeyBar("Exit [*]", "Apply", "Back"); + } else { + // Top Preview & Metadata Card (y=28..134) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 106); + XSetForeground(dpy, gc, pt.spec.border); + XDrawRectangle(dpy, pix, gc, 4, 28, kWinWidth - 9, 105); + + // Thumbnail frame at x=8, y=31, 76x100 + if (cachedImgValid && thumbXImg) { + XPutImage(dpy, pix, gc, thumbXImg, 0, 0, 8, 31, kThumbW, kThumbH); + } else { + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 8, 31, kThumbW, kThumbH); + drawPango(layoutSmall, &colMuted, 8, 70, 20, "No Image", kThumbW, 1); + } + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 7, 30, kThumbW + 1, kThumbH + 1); + + if (!items.empty() && selIdx >= 0 && + selIdx < static_cast(items.size())) { + const auto& cur = items[selIdx]; + bool isAct = (cur.path == activePath); + + drawPango(layoutMain, &colWhite, 90, 31, 20, cur.displayName, 142); + + std::string dimStr = cachedImgValid + ? (std::to_string(cachedImgW) + "x" + + std::to_string(cachedImgH)) + : "Unknown"; + drawPango(layoutSmall, &colLight, 90, 51, 18, + dimStr + " | " + formatFileSize(cur.sizeBytes), 142); + + drawPango(layoutSmall, isAct ? &colGreen : &colMuted, 90, 69, 18, + isAct ? "Status: [ACTIVE]" : ("Source: " + cur.sourceTag), + 142); + + drawPango(layoutSmall, &colBlue, 90, 89, 18, + std::string("Fit: < ") + kScaleModes[scaleModeIdx].label + + " >", + 142); + drawPango(layoutSmall, &colMuted, 90, 109, 18, + "[*] Fullscreen [< >] Fit", 142); + } + + // Scrollable List (y=138..266, 4 rows of 32px) + constexpr int kVisibleRows = 4; + int total = static_cast(items.size()); + int startIdx = 0; + if (total > kVisibleRows) { + startIdx = std::clamp(selIdx - kVisibleRows / 2, 0, + total - kVisibleRows); + } + + for (int i = 0; i < kVisibleRows; ++i) { + int idx = startIdx + i; + if (idx >= total) break; + const auto& e = items[idx]; + int ry = 138 + i * 32; + bool isSel = (idx == selIdx); + bool isAct = (e.path == activePath); + + XSetForeground(dpy, gc, isSel ? pt.spec.selBg + : (i % 2 == 0 ? pt.spec.bgAltEven + : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 30); + if (isAct) { + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, 4, ry, 3, 30); + } + + drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 5, 20, + e.displayName, 162); + if (isAct) { + drawPango(layoutSmall, &colGreen, 174, ry + 6, 18, "[ACTIVE]", 58, 2); + } else { + drawPango(layoutSmall, &colMuted, 174, ry + 6, 18, e.sourceTag, 58, 2); + } + } + + drawSoftkeyBar("View [*]", "Apply", "Close"); + } + + if (!statusBanner.empty()) { + int by = kWinHeight - 46; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19); + drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner, + kWinWidth - 20, 1); + } + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + auto showWindow = [&]() { + closeOtherPlugins("wallpapers"); + fullscreenPreview = false; + refreshData(); + for (size_t i = 0; i < items.size(); ++i) { + if (items[i].path == activePath) { + selIdx = static_cast(i); + break; + } + } + updatePreviewCache(); + writeIntFile(activePidPath, static_cast(::getpid())); + visible = true; + startupIgnoreUntilMs = nowMs() + 180; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + XMapRaised(dpy, win); + XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); + render(); + }; + + auto hideWindow = [&]() { + if (!visible) return; + visible = false; + fullscreenPreview = false; + softkeyPending = 0; + XUnmapWindow(dpy, win); + ::unlink(activePidPath.c_str()); + XFlush(dpy); + }; + + auto applySelectedWallpaper = [&]() { + if (items.empty() || selIdx < 0 || + selIdx >= static_cast(items.size())) { + return; + } + activePath = items[selIdx].path; + applyAndSaveWallpaper(activePath, scaleModeIdx); + setBanner("Applied: " + items[selIdx].displayName); + }; + + if (!isPreload) { + showWindow(); + } + + 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; + } + + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '*') { + fullscreenPreview = !fullscreenPreview; + render(); + continue; + } else if (dch == '#') { + scaleModeIdx = (scaleModeIdx + 1) % kNumScaleModes; + if (!activePath.empty()) { + applyAndSaveWallpaper(activePath, scaleModeIdx); + } + render(); + continue; + } + + if (sym == XK_Left || sym == XK_KP_Left) { + scaleModeIdx = (scaleModeIdx + kNumScaleModes - 1) % kNumScaleModes; + render(); + } else if (sym == XK_Right || sym == XK_KP_Right) { + scaleModeIdx = (scaleModeIdx + 1) % kNumScaleModes; + render(); + } else if (sym == XK_Up || sym == XK_KP_Up) { + if (!items.empty()) { + selIdx = (selIdx + static_cast(items.size()) - 1) % + static_cast(items.size()); + } + render(); + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (!items.empty()) { + selIdx = (selIdx + 1) % static_cast(items.size()); + } + render(); + } else if (sym == XK_Return || sym == XK_KP_Enter) { + applySelectedWallpaper(); + 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) { + fullscreenPreview = !fullscreenPreview; + render(); + } else if (pend == 2) { + if (fullscreenPreview) { + fullscreenPreview = false; + render(); + } else { + hideWindow(); + } + } + } 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(); + cachedImgPath.clear(); + if (visible) render(); + } else if (cmd == "TOGGLE") { + if (visible) hideWindow(); + else showWindow(); + } else if (cmd.rfind("OPEN", 0) == 0) { + showWindow(); + } + } + } + } + + hideWindow(); + if (thumbXImg) { + thumbXImg->data = nullptr; + XDestroyImage(thumbXImg); + } + if (fullXImg) { + fullXImg->data = nullptr; + XDestroyImage(fullXImg); + } + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + if (xftDraw) XftDrawDestroy(xftDraw); + XFreeGC(dpy, gc); + XFreePixmap(dpy, pix); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} + +int pluginInit(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("wallpapers", "--preload"); + } + return 0; +} + +void pluginCleanup(void) {} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "wallpapers", + "Bananarchy Desktop Wallpaper Manager (240x320)", + pluginInit, + pluginCleanup, + nullptr, + nullptr, + nullptr, + runWallpapersApp +}; diff --git a/src/sxmo-dmenu/dmenu.c b/src/sxmo-dmenu/dmenu.c index bd9cd9d..1fa2442 100644 --- a/src/sxmo-dmenu/dmenu.c +++ b/src/sxmo-dmenu/dmenu.c @@ -2001,6 +2001,10 @@ open_builtin_menu(const char *menu_name, int keep_idx) } if (!has_themes) add_menu_item(label, "@plugin:themes", 0); + snprintf(label, sizeof(label), "%s Wallpapers", ICON_THM); + add_menu_item(label, "@plugin:wallpapers", 0); + snprintf(label, sizeof(label), "%s Ringtones", ICON_BEL); + add_menu_item(label, "@plugin:ringtones", 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); @@ -2046,6 +2050,8 @@ open_builtin_menu(const char *menu_name, int keep_idx) 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 AT Command Console", ICON_TRM); + add_menu_item(label, "@plugin:modem:at", 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); @@ -2066,6 +2072,9 @@ open_builtin_menu(const char *menu_name, int keep_idx) snprintf(label, sizeof(label), "%s Volume & Controls (XDrawer)", ICON_MUS); add_menu_item(label, "@plugin:xdrawer", 0); + snprintf(label, sizeof(label), "%s Ringtones & Ringer", ICON_BEL); + add_menu_item(label, "@plugin:ringtones", 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); @@ -2129,6 +2138,10 @@ open_builtin_menu(const char *menu_name, int keep_idx) snprintf(label, sizeof(label), "%s Themes", ICON_THM); add_menu_item(label, "@plugin:themes", 0); + snprintf(label, sizeof(label), "%s Wallpapers", ICON_THM); + add_menu_item(label, "@plugin:wallpapers", 0); + snprintf(label, sizeof(label), "%s Ringtones", ICON_BEL); + add_menu_item(label, "@plugin:ringtones", 0); snprintf(label, sizeof(label), "%s Brightness", ICON_ARU); add_menu_item(label, "@toggle:bright_up", 1); snprintf(label, sizeof(label), "%s Brightness", ICON_ARD); @@ -2295,6 +2308,8 @@ close_other_active_plugins(void) { "xthemes.pid", "/tmp/dwm_plugin_themes.fifo" }, { "xmodem.pid", "/tmp/dwm_plugin_modem.fifo" }, { "xnetworks.pid", "/tmp/dwm_plugin_networks.fifo" }, + { "xringtones.pid", "/tmp/dwm_plugin_ringtones.fifo" }, + { "xwallpapers.pid", "/tmp/dwm_plugin_wallpapers.fifo" }, }; const char *rdir = getenv("XDG_RUNTIME_DIR"); const char *base = (rdir && rdir[0]) ? rdir : "/run/user/10000"; diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp index 667fbf0..b571341 100644 --- a/src/xdrawer/xdrawer.cpp +++ b/src/xdrawer/xdrawer.cpp @@ -784,6 +784,8 @@ static int xdrawer_run_main(int argc, char** argv) { closeOtherPlugin("xthemes.pid", "/tmp/dwm_plugin_themes.fifo"); closeOtherPlugin("xmodem.pid", "/tmp/dwm_plugin_modem.fifo"); closeOtherPlugin("xnetworks.pid", "/tmp/dwm_plugin_networks.fifo"); + closeOtherPlugin("xringtones.pid", "/tmp/dwm_plugin_ringtones.fifo"); + closeOtherPlugin("xwallpapers.pid", "/tmp/dwm_plugin_wallpapers.fifo"); theme.checkAndReload(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, nullptr, win); openStartMs = nowMs();