Add ringtones/wallpapers plugins, AT console, dialer USSD/*#06#, and SIM icon fix
This commit is contained in:
@@ -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_<name>.fifo`), softkey chord detection, Sxmo TSV contact/call/SMS parsers, and `libjpeg`/`libpng` helpers.
|
||||
- `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `callscreen_plugin.cpp` (`callscreen.so` / `xcallscreen`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`), `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`).
|
||||
|
||||
@@ -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 <name> --preload` with an exclusive `/tmp/dwm_plugin_<name>.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_<name>.fifo` in `< 2 ms`:
|
||||
#### 3.1.2 Preloaded DWM plugins (`DMenu`, `XDrawer`, `Dialer`, `Call Log`, `Call Screen`, `Contacts`, `SMS`, `Files`, `Camera`, `Themes`, `Modem`, `Networks`, `Ringtones`, `Wallpapers`)
|
||||
All fourteen core phone overlays are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup (`dwm-plugin <name> --preload` with an exclusive `/tmp/dwm_plugin_<name>.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_<name>.fifo` in `< 2 ms`:
|
||||
|
||||
1. **`DMenu` (`dmenu.so` / `dmenu` / `xdmenu`) — Zero-Lag System & Context Menus**:
|
||||
- **Open / Close**: Press **Left softkey (`Menu`)** for the **System menu** (`sys`) or **Right softkey (`Back`)** inside an application for its **Context menu** (`ctx`); press the same softkey or `Escape` to close.
|
||||
@@ -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 <ThemeName>` and `xthemes --export-default`.
|
||||
|
||||
11. **`Modem, Dual-SIM & PIN/PUK Manager` (`modem.so` / `xmodem`) — Dual-SIM Slot Provisioning, PIN/PUK Entry & Radio Controls**:
|
||||
- **Open**: Select **Modem** from the Sxmo **System menu** (`Left softkey`), or run `xmodem` (`xmodem unlock`, `xmodem info`, `xmodem log`, `xmodem ussd`). Also opens automatically when the active SIM card is PIN- or PUK-locked (`sxmo_hook_modem.sh`).
|
||||
- **Tabs (`Left`/`Right` or `1` / `2` / `3`)**:
|
||||
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`):
|
||||
|
||||
|
||||
@@ -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_<caller>_<timestamp>.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
|
||||
|
||||
@@ -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 &
|
||||
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
|
||||
feh --no-fehbg "$WP_FLAG" "$WP_FILE" >/dev/null 2>&1 </dev/null &
|
||||
pidof -s conky >/dev/null 2>&1 || CONKY_DPY=:0 conky -c /home/user/.config/sxmo/conky.conf -d >/dev/null 2>&1 </dev/null || true
|
||||
pidof -s fcitx5 >/dev/null 2>&1 || fcitx5 -d >/tmp/fcitx5.log 2>&1 </dev/null &
|
||||
sxmo_hook_start.sh >/tmp/sxmo_hook_start.log 2>&1 </dev/null &
|
||||
|
||||
Binary file not shown.
@@ -22,8 +22,24 @@ defaults() {
|
||||
xsetroot -solid '#1c1c1c'
|
||||
xset s off s noblank s 0 0 -dpms
|
||||
[ -e "$HOME"/.Xresources ] && xrdb -merge "$HOME"/.Xresources
|
||||
if [ -f /usr/share/wallpapers/bananarchy.png ] && command -v feh >/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
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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 <slot> <pin>|unblock-puk <slot> <puk> <newpin>|set-pin-lock <slot> <enable|disable> <pin>|change-pin <slot> <oldpin> <newpin>]" >&2
|
||||
echo "Usage: $0 [boot|status|switch <1|2>|verify-pin <slot> <pin>|unblock-puk <slot> <puk> <newpin>|set-pin-lock <slot> <enable|disable> <pin>|change-pin <slot> <oldpin> <newpin>|imei|usb-tether <on|off|status>|at <port> <cmd>]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
+1
@@ -0,0 +1 @@
|
||||
dwm-plugin
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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/
|
||||
|
||||
@@ -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<int>(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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int>(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<std::string, std::string>& 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<int>(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<int>(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<int>(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<AtTextMode>(
|
||||
(static_cast<int>(atTextMode) + 1) % 4);
|
||||
atLastTapKey = -1;
|
||||
render();
|
||||
continue;
|
||||
}
|
||||
if (dch == '*') {
|
||||
const char* syms = "+?=,1\"':;/-_*#";
|
||||
int len = static_cast<int>(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<int>(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<char>(
|
||||
std::tolower(static_cast<unsigned char>(chOut)));
|
||||
}
|
||||
atInput.back() = chOut;
|
||||
} else if (atInput.size() < 64) {
|
||||
atLastTapKey = digit;
|
||||
atLastTapIdx = 0;
|
||||
char chOut = seq[0];
|
||||
if (atTextMode == AtTextMode::Lower) {
|
||||
chOut = static_cast<char>(
|
||||
std::tolower(static_cast<unsigned char>(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<TabId>((static_cast<int>(activeTab) + 2) % 3);
|
||||
activeTab = static_cast<TabId>((static_cast<int>(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<TabId>((static_cast<int>(activeTab) + 1) % 3);
|
||||
activeTab = static_cast<TabId>((static_cast<int>(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<int>(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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <fstream>
|
||||
#include <poll.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
@@ -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<SmsThread> 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<struct sockaddr*>(&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<const struct sockaddr*>(&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<ssize_t>(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<uint8_t>(subType & 0xFF), 0x00, 0x00, 0x00
|
||||
};
|
||||
::sendto(sfd, bindReq, sizeof(bindReq), 0,
|
||||
reinterpret_cast<const struct sockaddr*>(&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<const struct sockaddr*>(&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<size_t>(buf[5]) |
|
||||
(static_cast<size_t>(buf[6]) << 8);
|
||||
size_t pos = 7;
|
||||
size_t end = std::min(static_cast<size_t>(rlen), 7 + tlvLen);
|
||||
while (pos + 3 <= end) {
|
||||
uint8_t tType = buf[pos];
|
||||
size_t tLen = static_cast<size_t>(buf[pos + 1]) |
|
||||
(static_cast<size_t>(buf[pos + 2]) << 8);
|
||||
pos += 3;
|
||||
if (pos + tLen > end) break;
|
||||
if (tType == 0x11 && tLen > 0) {
|
||||
imeiOut.assign(reinterpret_cast<const char*>(buf + pos), tLen);
|
||||
imeiOut = trimStr(imeiOut);
|
||||
} else if (tType == 0x13 && tLen > 0 && svOut.empty()) {
|
||||
svOut.assign(reinterpret_cast<const char*>(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
|
||||
|
||||
@@ -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 <X11/Xatom.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <pango/pangoxft.h>
|
||||
#include <dirent.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <clocale>
|
||||
|
||||
#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<int>(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0]));
|
||||
|
||||
constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50};
|
||||
constexpr int kNumRingLoopSteps =
|
||||
static_cast<int>(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<char>(std::toupper(static_cast<unsigned char>(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<RingtoneEntry>& out) {
|
||||
DIR* d = ::opendir(dirPath.c_str());
|
||||
if (!d) return;
|
||||
std::vector<std::string> 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<RingtoneEntry> discoverRingtones() {
|
||||
std::vector<RingtoneEntry> 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<int>(::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<RingtoneEntry> 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<int>(tones.size())) {
|
||||
selIdx = std::max(0, static_cast<int>(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<int>(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<int>(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<int>(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<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
writeIntFile(activePidPath, static_cast<int>(::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<KeySym>(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<int>(tones.size()) - 1) %
|
||||
static_cast<int>(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<int>(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<int>(rem);
|
||||
}
|
||||
}
|
||||
if (!statusBanner.empty()) {
|
||||
int64_t bRem = statusBannerUntilMs - now;
|
||||
if (bRem <= 0) {
|
||||
statusBanner.clear();
|
||||
render();
|
||||
} else if (timeoutMs < 0 || bRem < timeoutMs) {
|
||||
timeoutMs = static_cast<int>(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
|
||||
};
|
||||
@@ -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 <X11/Xatom.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <pango/pangoxft.h>
|
||||
#include <dirent.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <clocale>
|
||||
|
||||
#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<int>(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<char>(std::toupper(static_cast<unsigned char>(stem[0])));
|
||||
}
|
||||
return trimStr(stem);
|
||||
}
|
||||
|
||||
void scanWallpaperDir(const std::string& dirPath,
|
||||
const std::string& sourceTag,
|
||||
std::vector<WallpaperEntry>& out) {
|
||||
DIR* d = ::opendir(dirPath.c_str());
|
||||
if (!d) return;
|
||||
std::vector<std::string> 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<WallpaperEntry> discoverWallpapers() {
|
||||
std::vector<WallpaperEntry> 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<int>(::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<WallpaperEntry> 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<uint8_t> cachedRgb;
|
||||
int cachedImgW = 0;
|
||||
int cachedImgH = 0;
|
||||
bool cachedImgValid = false;
|
||||
|
||||
constexpr int kThumbW = 76;
|
||||
constexpr int kThumbH = 100;
|
||||
std::vector<uint32_t> thumbPixels(kThumbW * kThumbH, 0x111111);
|
||||
XImage* thumbXImg = XCreateImage(
|
||||
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
|
||||
reinterpret_cast<char*>(thumbPixels.data()), kThumbW, kThumbH, 32, 0);
|
||||
|
||||
constexpr int kFullW = 240;
|
||||
constexpr int kFullH = 246;
|
||||
std::vector<uint32_t> fullPixels(kFullW * kFullH, 0x111111);
|
||||
XImage* fullXImg = XCreateImage(
|
||||
dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0,
|
||||
reinterpret_cast<char*>(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<int>(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<int>(items.size())) {
|
||||
selIdx = std::max(0, static_cast<int>(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<int>(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<int>(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<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
updatePreviewCache();
|
||||
writeIntFile(activePidPath, static_cast<int>(::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<int>(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<KeySym>(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<int>(items.size()) - 1) %
|
||||
static_cast<int>(items.size());
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||||
if (!items.empty()) {
|
||||
selIdx = (selIdx + 1) % static_cast<int>(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<int>(rem);
|
||||
}
|
||||
}
|
||||
if (!statusBanner.empty()) {
|
||||
int64_t bRem = statusBannerUntilMs - now;
|
||||
if (bRem <= 0) {
|
||||
statusBanner.clear();
|
||||
render();
|
||||
} else if (timeoutMs < 0 || bRem < timeoutMs) {
|
||||
timeoutMs = static_cast<int>(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
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user