diff --git a/AGENTS.md b/AGENTS.md index adeaa0f..98afa7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f - `src/dwm-plugins/`: Preloaded C++20 `240x320` DWM plugins and shared headers: - `theme.h`: Unified `.Xresources` / `~/.xthemes/*.xth` parser, proportional font sizing (`theme.baseFontSize`), automatic shade derivation, and live Xft/Pango theme reloading (`PluginTheme`). - `phone_data.h`: Shared X11 geometry (`240x296` below the 24px bar), FIFO IPC (`/tmp/dwm_plugin_.fifo`), softkey chord detection, Sxmo TSV contact/call/SMS parsers, and `libjpeg`/`libpng` helpers. - - `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`). - - `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `dmenu`, and `xdmenu`. + - `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `callscreen_plugin.cpp` (`callscreen.so` / `xcallscreen`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`). + - `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcallscreen`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `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`). @@ -23,7 +23,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`, `contacts`, `sms`, `files`, `camera`, `themes`). +- `./scripts/build-sxmo-x11.sh` — Builds `st`, `dmenu.so`, `dwm`, `dwm-plugin`, and all C++ `.so` plugins (`xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`). - `./scripts/build-fcitx5-t9.sh` — Builds `src/fcitx5-t9` (`libt9.so` / `t9.so`). - `./scripts/build-kernel-audio.sh` — Builds `6.12.1-msm8916` QDSP6 audio, torch LED, camera `.ko` modules, and device `.dtb` files. - `./scripts/build-xlibre.sh` — Builds Xlibre `Xorg` and `libinput`/`fbdev` DDX drivers. diff --git a/MANUAL.md b/MANUAL.md index 739a660..fe2b34c 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -29,7 +29,7 @@ This manual covers flashing, operating, and customizing **Bananarchy Linux** (`v - **Patched `sxmo-st` (`st`) with HarfBuzz ligatures & XIM**: patched with `libharfbuzz` (`hb.c`/`hb.h`) shaping (`liga`, `calt`, `clig`, `dlig`) so multi-character programming operators (`->`, `=>`, `!=`, `<=`, `>=`, `===`, `::`, `//`, `/*`) render as continuous ligatures at `pixelsize=18`, alongside full `XIM` (`fcitx5-t9`) input support. - **Patched `sxmo-dmenu` plugin (`dmenu.so` / `dmenu` / `xdmenu`) (`< 2 ms` popup/close)**: preloaded resident DWM plugin rendering a full-screen `240x296` 10-line vertical menu (positioned flush at `Y = 24..320` right below the 24px `dwm` bar, with a `26px` search bar and ten `27px` item rows) with a `#383838` top search bar, `#005577` prompt badge, `#1c1c1c` item list, `XIM` (`XSetLocaleModifiers("")`, `XSetICFocus`, `Xutf8LookupString`) for live `fcitx5-t9` search filtering and `#` mode switching, C-level focus preservation (`prevfocuswin`), built-in `< 0.5 ms` in-place Sxmo menu transitions (`Sys`, `Apps`, `Scripts`, `Modem`, `Config`, `Power`, `St`, `Nano`), and a `/tmp/dwm_plugin_dmenu.sock` IPC server for all `/usr/bin/dmenu` and `sxmo_dmenu.sh` calls. - **Patched `sxmo-dwm` (`dwm`) (`< 2 ms` display wake/unlock & native plugin system)**: configured for `240x320` (`bh = 24`, `#323232` top bar, `#005577` active tag/border, vertical `bstack` layout default, overflow-protected Pango status bar rendering keeping the right-aligned clock always visible, and a transparent-backgrounded bottom desktop softkey bar displaying `"Menu"` and `"Names"` over the wallpaper). Directly polls `/dev/input/by-path/platform-gpio-keys-event` (`SW_LID`) and intercepts locked-state wake keys (`XF86PowerOff`, `XF86PickupPhone`, `XF86KbdLcdMenu1`, `XF86KbdLcdMenu2`) in C to toggle `/sys/class/backlight/backlight/brightness` and `/sys/class/leds/white:kbd_backlight/brightness` in `< 2 ms` before running Sxmo state hooks asynchronously. - - **Preloaded DWM plugins (`src/sxmo-dmenu/`, `src/dwm-plugins/` & `src/xdrawer/`, `< 2 ms` FIFO/socket wakeup & unified `.Xresources` theming)**: `dwm` dynamically loads `.so` plugins from `/usr/lib/dwm/plugins/` and `~/.local/lib/dwm/plugins/` (`dmenu.so`, `xdrawer.so`, `dialer.so`, `calllog.so`, `contacts.so`, `sms.so`, `files.so`, `camera.so`, `themes.so`) and preloads them as resident background daemons (`dwm-plugin --preload`) at startup. Each daemon keeps its X11 window, offscreen `Pixmap`, Xft/Pango layouts, and `stat()`-cached Sxmo TSV state initialized in memory while sleeping at 0% CPU on `/tmp/dwm_plugin_.fifo`. Every UI element across `dwm`, `dmenu`, `st`, and all C++ plugins obeys a single unified color and font scheme configured via `~/.Xresources` (`theme.*`, `dwm.*`, `dmenu.*`, `st.*`), with proportional font sizing derived from `theme.baseFontSize` (`12px` / `15px` / `16px` / `18px` at default `15`) and instant live reload via `xthemes`. + - **Preloaded DWM plugins (`src/sxmo-dmenu/`, `src/dwm-plugins/` & `src/xdrawer/`, `< 2 ms` FIFO/socket wakeup & unified `.Xresources` theming)**: `dwm` dynamically loads `.so` plugins from `/usr/lib/dwm/plugins/` and `~/.local/lib/dwm/plugins/` (`dmenu.so`, `xdrawer.so`, `dialer.so`, `calllog.so`, `callscreen.so`, `contacts.so`, `sms.so`, `files.so`, `camera.so`, `themes.so`) and preloads them as resident background daemons (`dwm-plugin --preload`) at startup. Each daemon keeps its X11 window, offscreen `Pixmap`, Xft/Pango layouts, and `stat()`-cached Sxmo TSV state initialized in memory while sleeping at 0% CPU on `/tmp/dwm_plugin_.fifo`. Every UI element across `dwm`, `dmenu`, `st`, and all C++ plugins obeys a single unified color and font scheme configured via `~/.Xresources` (`theme.*`, `dwm.*`, `dmenu.*`, `st.*`), with proportional font sizing derived from `theme.baseFontSize` (`12px` / `15px` / `16px` / `18px` at default `15`) and instant live reload via `xthemes`. 2. **Multi-mode `fcitx5-t9` keypad engine (`src/fcitx5-t9/`)**: - Supports 6 input modes cycled with `#` (`XF86NumericPound`): `abc`, `Abc`, `ABC`, `Sym`, `Ctl`, and `123`, updating the live top-bar status indicator (`[abc]`, `[Abc]`, `[ABC]`, `[Sym]`, `[Ctl]`, `[123]`) in real time. - Locks the candidate popup bar to the bottom of the screen (`X = 1, Y = 290, W = 238, H = 30`) with fixed-width 28-character monospace formatting (`Fira Code 9.5`), so neither the popup window nor individual letters ever jump when cycling through candidates. @@ -141,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`, `Contacts`, `SMS`, `Files`, `Camera`, `Themes`) -All nine core phone overlays are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup (`dwm-plugin --preload` with an exclusive `/tmp/dwm_plugin_.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_.fifo` in `< 2 ms`: +#### 3.1.2 Preloaded DWM plugins (`DMenu`, `XDrawer`, `Dialer`, `Call Log`, `Call Screen`, `Contacts`, `SMS`, `Files`, `Camera`, `Themes`) +All ten core phone overlays are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup (`dwm-plugin --preload` with an exclusive `/tmp/dwm_plugin_.lock` single-instance guard), and awakened over `/tmp/dwm_plugin_.fifo` in `< 2 ms`: 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. @@ -162,29 +162,40 @@ All nine core phone overlays are compiled as shared-object plugins (`/usr/lib/dw - **Open**: On the empty desktop, press **Call (green)** (`XF86PickupPhone` / `XF86Phone`), or select **Call Log** from the Sxmo menu or Dialer. - **Controls**: `Left`/`Right` switches between **`All`**, **`Missed`**, **`In`**, and **`Out`** filter tabs; `Up`/`Down` scrolls through call entries (showing direction icon, resolved contact name or number, timestamp, and call duration); **Call (green)** or **Center `OK` (`Return`)** dials the selected entry; **Left softkey (`Dialer`)** opens the selected number in the Dialer; **Right softkey (`Close`)** or `Escape` closes the Call Log. -5. **`Contacts` (`contacts.so` / `xcontacts` / `sxmo_contactmenu.sh`) — Full-Featured Phonebook**: +5. **`Call Screen` (`callscreen.so` / `xcallscreen`) — Incoming & Active Call Manager**: + - **Automatic Invocation**: Automatically wakes the display and opens on incoming calls (`sxmo_modem.sh checkforincomingcalls` -> `xcallscreen incoming `) and outgoing calls (`sxmo_modemdial.sh` / `placePhoneCall()` -> `xcallscreen outgoing `). Pressing **Call (green)** while a call is active reopens `xcallscreen` from anywhere. + - **Incoming Call Controls**: Displays the resolved contact name from `~/.config/sxmo/contacts.tsv`, phone number, `INCOMING CALL` badge, and ring timer. Press **Call (green)**, **Left softkey (`Accept`)**, **`1`**, or **Center `OK` (`Return`)** (on `Answer Call`) to accept the call; press **`*`** or **`0`** (or `OK` on `Silence Ring`) to mute the ringtone and vibration while keeping the call screen open; press **End (red)**, **Right softkey (`Reject`)**, **`#`**, or `OK` on `Reject Call` to decline the call. + - **In-Call Controls & Call Recording**: Displays live `MM:SS` call duration and four interactive control rows (`Up`/`Down` + `OK` or direct hotkeys): + - **`[1]` / `*` — Microphone Mute**: toggles hardware QDSP6 (`DEC1 MUX`, `ADC3 Volume`, `VoiceMMode1 Capture Mixer`) and PipeWire microphone mute (`ON` / `MUTED`). + - **`[2]` / `#` — Call Recording**: starts/stops recording call audio via `pw-record` (`16 kHz` mono WAV) to `~/Recordings/call__YYYYMMDD_HHMMSS.wav` with a live `REC MM:SS` indicator, automatically finalizing the recording when the call ends. + - **`[3]` — Audio Route**: switches call audio between **`EARPIECE`** and **`SPEAKER`** (`sxmo_audio.sh` + ALSA `EAR_S` / `SPK DAC Switch`). + - **`Left` / `Right` — Call Volume**: adjusts in-call volume in `5%` increments (`5%`–`100%`). + - **`[0]` / `Left softkey (`DTMF`)` — DTMF Keypad Mode**: toggles DTMF tone mode where pressing `0`–`9`, `*`, or `#` sends DTMF tones via `mmcli --send-dtmf`. + - **End (red)** or **Right softkey (`Hangup`)**: hangs up the active call (`sxmo_modemcall.sh hangup`). + +6. **`Contacts` (`contacts.so` / `xcontacts` / `sxmo_contactmenu.sh`) — Full-Featured Phonebook**: - **Open**: On the empty desktop, press the **Right softkey (`Names`)**, or select **Contacts** from the Sxmo menu. - **Phonebook List & Live Search**: Typing `0`–`9` filters contacts in real time by T9 name or phone number; `Up`/`Down` scrolls; **Call (green)** immediately calls the selected contact; **Center `OK` (`Return`)** opens the contact's details & actions card; **Left softkey (`Options`)** opens the options menu (**New Contact**, **Add from Recent**, **Call Contact**, **Send SMS**, **Edit Contact**, **Delete Contact**). - **Contact Details View**: Provides one-click actions for **Call Contact**, **Send SMS**, **Read Messages**, **Open in Dialer**, **Edit Contact**, and **Delete Contact**. - **Built-in Contact Editor**: Supports adding new contacts or editing existing contacts directly in the `240x320` UI. In the **Name** field, keypad keys `0`–`9` provide multi-tap character entry (`#` cycles case mode `[Abc]` -> `[ABC]` -> `[abc]` -> `[123]`, `*` inserts space). In the **Number** field, hold `0` or press `Right` to insert `+`, or select **Use Recent Number** to pick from recent unknown callers in `modemlog.tsv`. Press **Left softkey (`Save`)** or select **Save Contact** to write directly to `~/.config/sxmo/contacts.tsv`. -6. **`SMS Manager / Editor` (`sms.so` / `xsms` / `sxmo_modemtext.sh`) — Conversations & Composer**: +7. **`SMS Manager / Editor` (`sms.so` / `xsms` / `sxmo_modemtext.sh`) — Conversations & Composer**: - **Open**: Select **Texts** from the Sxmo system menu (`Left softkey`), or choose **Send SMS** / **Read Messages** from Contacts. - **Conversations List**: Shows all SMS threads and drafts from `~/.local/share/sxmo/modem//sms.txt` with contact name resolution, message previews, and timestamps. Press `0`–`9` for live T9/number search, `*` for a new message, **Center `OK` (`Return`)** to open the conversation, **Call (green)** to call the sender, or **Left softkey (`Options`)** for **New Message**, **Open Thread**, **Reply / Compose**, **Call Number**, **Save/View Contact**, and **Delete Thread**. - **Thread View**: Displays incoming (`Recv`, green left accent) and outgoing (`Sent`, blue/gold right accent) message bubbles; `Up`/`Down` scrolls; **Center `OK` (`Return`)** opens the reply composer; **Call (green)** calls the contact. - **Multi-Tap SMS Composer**: Supports picking a recipient from Contacts/Recents or entering a number directly (hold `0` for `+`), multi-tap text entry on keys `0`–`9` and `*` (`#` cycles `[Abc]` -> `[ABC]` -> `[abc]` -> `[123]`), live character and SMS segment counter (`0/160`), automatic draft persistence (`draft.txt`), dynamic center softkey action (`Send` when focused on Recipient/Message vs. `Draft` when focused on the `Save Draft` button), and asynchronous sending via `sxmo_modemsendsms.sh` (`Call` key, `Send SMS` button, or `Left softkey`). -7. **`File Manager` (`files.so` / `xfiles` / `sxmo_files.sh`) — Graphical Directory Browser & Viewer**: +8. **`File Manager` (`files.so` / `xfiles` / `sxmo_files.sh`) — Graphical Directory Browser & Viewer**: - **Open**: Select **Files** from the Sxmo system menu (`Left softkey`) or press `Mod4+d`. - **Browser Controls**: `Up`/`Down` navigates files and directories; `Return`/`Right` enters a directory or opens the selected file; `Left`/`BackSpace` ascends to the parent directory (`..`); `0`–`9` filters items in real time; `*` toggles hidden dotfiles (`.*`); `#` cycles sort order (**Name** -> **Date** -> **Size**). - **Built-in Viewers & File Actions (`Left softkey`)**: Text files and scripts open in a fast built-in scrollable viewer (with `Left softkey` shortcut to edit in `st -e nano`); JPEG and PNG images open in a built-in `libjpeg`/`libpng` previewer (`Left`/`Right` steps through images in the folder, `Left softkey` opens full screen in `feh`). The **Actions** menu supports **Open**, **Edit in Nano**, **New Folder**, **New File**, **Rename**, **Copy**, **Cut / Move**, **Paste Here**, **Properties**, **Toggle Hidden (`*`)**, **Cycle Sort (`#`)**, **Open Terminal Here**, and **Delete**. -8. **`Camera` (`camera.so` / `xcamera`) — Live Viewfinder & Photo Gallery**: +9. **`Camera` (`camera.so` / `xcamera`) — Live Viewfinder & Photo Gallery**: - **Open**: Select **Camera** from the Sxmo system menu (`Left softkey`) or run `xcamera` / `bananarchy-photo --menu`. - **Live Viewfinder**: Streams `800x600` `RGB24` frames directly from `/dev/video0` (`msm8905-cam` / `GC2395`) into a `240x180` 4:3 viewfinder with a center reticle and a 1-second V4L2 `poll()` frame watchdog. Press **Center `OK` (`Return`)**, **`5`**, or **Call (green)** to snap a JPEG photo to `~/Pictures/photo-YYYYMMDD-HHMMSS.jpg`; `*` or `1` toggles **Auto-Flash**; `0` or `2` toggles continuous **Torch** (`white:torch`); `Up`/`Down` adjusts **Digital Zoom** (`1.0x` / `2.0x` / `4.0x`); `#` or `3` toggles the **3-second Self-Timer**; **Left softkey (`Gallery`)** opens the built-in photo gallery. - **Built-in Photo Gallery**: Browse captured photos with `Left`/`Right` or `Up`/`Down`, press `Return` to open full-screen in `feh`, or press **Left softkey (`Delete`)** to delete a photo. -9. **`Theme Manager` (`themes.so` / `xthemes`) — Unified `.Xresources` Theme Browser & Customizer (`~/.xthemes/*.xth`)**: +10. **`Theme Manager` (`themes.so` / `xthemes`) — Unified `.Xresources` Theme Browser & Customizer (`~/.xthemes/*.xth`)**: - **Open**: Select **Themes** from the Sxmo **System menu**, **Apps** menu, or **Config** menu, or run `xthemes`. - **Theme Files (`~/.xthemes/*.xth`)**: Every `.xth` file in `~/.xthemes/` is a standard `Xresources` file defining `theme.name`, `theme.fontFace`, `theme.iconFontFace`, `theme.baseFontSize` (from which `sizeSmall`, `sizeMain`, `sizeIcon`, and `sizeLarge` are proportionally derived across all UI elements), core and surface colors (`theme.bgMain`, `theme.bgBar`, `theme.bgHeader`, `theme.selBg`, `theme.selFg`, `theme.accent`, `theme.fgMain`, `theme.fgMuted`, etc.), and synchronized `dwm.*`, `dmenu.*`, `st.*`, and `svkbd.*` resources. Pre-installed themes include **`Default.xth`**, **`BananaGold.xth`**, **`MatrixGreen.xth`**, **`NordFrost.xth`**, and **`SolarizedLight.xth`**. - **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. diff --git a/README.md b/README.md index ced6402..bf32491 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,12 @@ Bananarchy targets an ultra-lightweight **Sxmo X11 (`sxmo-de-dwm`)** environment - **Cross-compiled Xlibre (`X11Libre/xserver` 25.2.0) display server**: `modesetting` KMS + `ShadowFB` Damage partial-rectangle SPI transfers, software cursor fallback, and rootless `elogind` integration. - **Patched `sxmo-dwm` with native C/C++ plugin architecture**: `240x320` vertical `bstack` layout, 24px Pango status bar with an overflow-protected right-aligned clock, transparent-backgrounded `"Menu"` and `"Names"` desktop softkey labels, direct C `SW_LID` slider and locked-key wake handling in `< 2 ms`, and a dynamic `.so` plugin API (`dwm_plugin.h`) with preloaded resident daemons (`--preload`) and non-blocking FIFO IPC (`< 10 ms` wakeup latency). -- **Preloaded `240x320` keypad-driven DWM plugins (`dmenu`, `xdrawer`, `dialer`, `calllog`, `contacts`, `sms`, `files`, `camera`, `themes`)**: +- **Preloaded `240x320` keypad-driven DWM plugins (`dmenu`, `xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`)**: - **`DMenu` (`dmenu.so` / `dmenu` / `xdmenu`)**: zero-lag (`< 2 ms` popup/close) resident `240x320` full-screen 10-line vertical menu (`240x296` flush below the 24px top bar) with a `#383838` search bar, `#005577` prompt badge, `XIM` `fcitx5-t9` text input, in-place `< 0.5 ms` native Sxmo submenu transitions (`Sys`, `Apps`, `Scripts`, `Modem`, `Config`, `Power`, `St`, `Nano`), and a `/tmp/dwm_plugin_dmenu.sock` client interface serving all `/usr/bin/dmenu` and `sxmo_dmenu.sh` pipes instantaneously. - **`XDrawer` (`xdrawer.so` / `xdrawer`)**: global phone controls overlay invoked from anywhere by pressing **both softkeys simultaneously**, providing instant sliders and toggles for speaker volume, earpiece volume, flashlight, sound mute, microphone mute, screen brightness, and keypad backlight. - **`Dialer` (`dialer.so` / `xdialer`)**: opens automatically from the idle desktop when pressing any **`0`–`9`, `*`, or `#` key** (inserting that character immediately), with live T9/number contact filtering, hold-`0` for `+`, and single-press **Call** dialing. - **`Call Log` (`calllog.so` / `xcalllog`)**: opens automatically from the idle desktop when pressing the **Call (green)** button, with `All` / `Missed` / `In` / `Out` tabs, contact resolution, call durations, and instant redial. + - **`Call Screen` (`callscreen.so` / `xcallscreen`)**: dedicated incoming, outgoing, and in-call screen integrated with Sxmo's `ModemManager` call lifecycle (`sxmo_modem.sh`, `sxmo_modemcall.sh`, `sxmo_modemdial.sh`), displaying resolved caller name and phone number, accepting/rejecting incoming calls via physical **Call (green)** and **End (red)** buttons (plus `*`/`0` ringtone silence), live `MM:SS` call timer, microphone mute/unmute (`1`/`*`), live call recording to `~/Recordings/call__.wav` (`2`/`#`), earpiece/speaker audio routing (`3`), volume control (`Left`/`Right`), and DTMF tone keypad mode (`0` / `Left softkey`). - **`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). - **`SMS Manager` (`sms.so` / `xsms`)**: conversation thread manager, bubble message history reader, and multi-tap keypad SMS composer/draft editor (with dynamic `Send` / `Draft` center softkey action) integrated with `$SXMO_LOGDIR//sms.txt`, `draft.txt`, `modemlog.tsv`, and `sxmo_modemsendsms.sh`. - **`File Manager` (`files.so` / `xfiles`)**: graphical directory browser with live T9/number filtering, dotfiles toggle (`*`), sort mode cycle (`#`), file operations (new folder/file, rename, copy, cut/move, paste, delete, properties, edit in `nano`, open terminal here), built-in text viewer, and built-in `libjpeg`/`libpng` image previewer. @@ -94,7 +95,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, contacts, sms, files, camera, themes), theme.h & runner +│ ├── dwm-plugins/ # DWM plugins (dialer, calllog, callscreen, contacts, sms, files, camera, themes), 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 @@ -107,7 +108,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, 8 C++ DWM plugins, and st (HarfBuzz ligatures) +│ ├── sxmo-x11/ # Prebuilt armv7 patched dwm, dmenu.so/dmenu, 9 C++ DWM plugins, and st (HarfBuzz ligatures) │ ├── kernel-6.12.1-msm8916/ # Prebuilt QDSP6/Q6Voice .ko, torch .ko, camera .ko, and 5 device DTBs │ ├── fcitx5-t9/ # Prebuilt armv7 libt9.so and fcitx5 addon/inputmethod configs │ └── bantam/ # Prebuilt armv7 bantam AI agent binary diff --git a/overlay/rootfs/etc/skel/.config/sxmo/hooks/sxmo_hook_inputhandler.sh b/overlay/rootfs/etc/skel/.config/sxmo/hooks/sxmo_hook_inputhandler.sh index 8246223..3ee80a2 100755 --- a/overlay/rootfs/etc/skel/.config/sxmo/hooks/sxmo_hook_inputhandler.sh +++ b/overlay/rootfs/etc/skel/.config/sxmo/hooks/sxmo_hook_inputhandler.sh @@ -401,8 +401,14 @@ case "$ACTION" in exit 0 ;; "enter_one") - if pidof -s sxmo_RepeatRing.sh >/dev/null; then - sxmo_hook_pickup.sh + if [ -e /tmp/sxmo_call_ringing ] || [ -e "$XDG_RUNTIME_DIR/sxmo_jobs/ringing" ] || pidof -s sxmo_RepeatRing.sh >/dev/null; then + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen accept + else + sxmo_hook_pickup.sh + fi + elif [ -e /tmp/sxmo_active_call ] && command -v xcallscreen >/dev/null 2>&1; then + xcallscreen else sxmo_appmenu.sh "$WMCLASS" "$WMNAME" fi diff --git a/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_inputhandler.sh b/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_inputhandler.sh index 8246223..3ee80a2 100755 --- a/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_inputhandler.sh +++ b/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_inputhandler.sh @@ -401,8 +401,14 @@ case "$ACTION" in exit 0 ;; "enter_one") - if pidof -s sxmo_RepeatRing.sh >/dev/null; then - sxmo_hook_pickup.sh + if [ -e /tmp/sxmo_call_ringing ] || [ -e "$XDG_RUNTIME_DIR/sxmo_jobs/ringing" ] || pidof -s sxmo_RepeatRing.sh >/dev/null; then + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen accept + else + sxmo_hook_pickup.sh + fi + elif [ -e /tmp/sxmo_active_call ] && command -v xcallscreen >/dev/null 2>&1; then + xcallscreen else sxmo_appmenu.sh "$WMCLASS" "$WMNAME" fi diff --git a/overlay/rootfs/usr/bin/dwm-plugin b/overlay/rootfs/usr/bin/dwm-plugin index 81ac988..9e3f0a3 100755 Binary files a/overlay/rootfs/usr/bin/dwm-plugin and b/overlay/rootfs/usr/bin/dwm-plugin differ diff --git a/overlay/rootfs/usr/bin/sxmo_modem.sh b/overlay/rootfs/usr/bin/sxmo_modem.sh index 314284a..4aa6025 100755 --- a/overlay/rootfs/usr/bin/sxmo_modem.sh +++ b/overlay/rootfs/usr/bin/sxmo_modem.sh @@ -113,6 +113,9 @@ checkforfinishedcalls() { fi sxmo_jobs.sh stop incall_menu sxmo_jobs.sh stop proximity_lock + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen ended >/dev/null 2>&1 || true + fi sxmo_hook_statusbar.sh state & if ! sxmo_modemaudio.sh reset_audio; then @@ -157,6 +160,15 @@ checkforincomingcalls() { mkdir -p "$SXMO_LOGDIR" printf %b "$TIME\tcall_ring\t$INCOMINGNUMBER\n" >> "$SXMO_LOGDIR/modemlog.tsv" + if sxmo_state.sh get | grep -qE "screenoff|lock"; then + sxmo_wm.sh display on >/dev/null 2>&1 || true + sxmo_state.sh set unlock & + fi + + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen incoming "$VOICECALLID" "$INCOMINGNUMBER" >/dev/null 2>&1 || true + fi + sxmo_jobs.sh start proximity_lock sxmo_proximitylock.sh sxmo_hook_statusbar.sh state & @@ -167,7 +179,7 @@ checkforincomingcalls() { # Refresh the incall menu sxmo_jobs.sh start incall_menu sxmo_modemcall.sh incall_menu else - # Or fire the incomming call menu + # Or fire the incoming call menu sxmo_jobs.sh start incall_menu sxmo_modemcall.sh incoming_call_menu "$VOICECALLID" fi diff --git a/overlay/rootfs/usr/bin/sxmo_modemcall.sh b/overlay/rootfs/usr/bin/sxmo_modemcall.sh new file mode 100755 index 0000000..3b54778 --- /dev/null +++ b/overlay/rootfs/usr/bin/sxmo_modemcall.sh @@ -0,0 +1,192 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2022 Sxmo Contributors + +# include common definitions +# shellcheck source=configs/default_hooks/sxmo_hook_icons.sh +. sxmo_hook_icons.sh +# shellcheck source=scripts/core/sxmo_common.sh +. sxmo_common.sh + +# We use this directory to store states, so it must exist +mkdir -p "$XDG_RUNTIME_DIR/sxmo_calls" + +set -e + +vid_to_number() { + mmcli -m any -o "$1" -K | \ + grep call.properties.number | \ + cut -d ':' -f2 | \ + tr -d ' ' +} + +log_event() { + EVT_HANDLE="$1" + EVT_VID="$2" + + NUM="$(vid_to_number "$EVT_VID")" + TIME="$(date +%FT%H:%M:%S%z)" + + mkdir -p "$SXMO_LOGDIR" + printf %b "$TIME\t$EVT_HANDLE\t$NUM\n" >> "$SXMO_LOGDIR/modemlog.tsv" +} + +pickup() { + CALLID="$1" + NUM="$(vid_to_number "$CALLID" 2>/dev/null || true)" + + DIRECTION="$( + mmcli --voice-status -o "$CALLID" -K | + grep call.properties.direction | + cut -d: -f2 | + tr -d " " + )" + case "$DIRECTION" in + outgoing) + if ! sxmo_modemaudio.sh setup_audio; then + sxmo_notify_user.sh --urgency=critical "We failed to setup call audio" + return 1 + fi + + if ! mmcli -m any -o "$CALLID" --start; then + sxmo_notify_user.sh --urgency=critical "We failed to start the call" + return 1 + fi + + sxmo_notify_user.sh "Started call" + touch "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.initiatedcall" + log_event "call_start" "$CALLID" + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen outgoing "$CALLID" "$NUM" >/dev/null 2>&1 || true + fi + ;; + incoming) + sxmo_log "Invoking pickup hook" + sxmo_hook_pickup.sh + + if ! sxmo_modemaudio.sh setup_audio; then + sxmo_notify_user.sh --urgency=critical "We failed to setup call audio" + return 1 + fi + + if ! mmcli -m any -o "$CALLID" --accept; then + sxmo_notify_user.sh --urgency=critical "We failed to accept the call" + return 1 + fi + + sxmo_notify_user.sh "Picked up call" + touch "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.pickedupcall" + log_event "call_pickup" "$CALLID" + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen active "$CALLID" "$NUM" >/dev/null 2>&1 || true + fi + ;; + *) + sxmo_notify_user.sh --urgency=critical "Couldn't initialize call with callid <$CALLID>; unknown direction <$DIRECTION>" + rm "$XDG_RUNTIME_DIR/sxmo_calls/"* 2>/dev/null || true + rm -f "$XDG_RUNTIME_DIR"/sxmo.ring.pid 2>/dev/null + rm -f "$SXMO_NOTIFDIR"/incomingcall* 2>/dev/null + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen ended >/dev/null 2>&1 || true + fi + return 1 + ;; + esac +} + +hangup() { + CALLID="$1" + + if [ -f "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.pickedupcall" ] || \ + [ -f "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.initiatedcall" ]; then + + rm -f "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.pickedupcall" \ + "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.initiatedcall" + touch "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.hangedupcall" + log_event "call_hangup" "$CALLID" + + sxmo_log "sxmo_modemcall: Invoking hangup hook" + sxmo_hook_hangup.sh + else + # this call was never picked up and hung up immediately, so it is a discarded call + touch "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.discardedcall" + log_event "call_discard" "$CALLID" + + sxmo_log "sxmo_modemcall: Invoking discard hook" + sxmo_hook_discard.sh + fi + + if ! mmcli -m any -o "$CALLID" --hangup; then + # we ignore already closed calls + if list_active_calls | grep -q "/$CALLID "; then + sxmo_notify_user.sh --urgency=critical "We failed to hangup the call" + return 1 + fi + fi +} + +# We shallow muted/blocked and terminated calls +list_active_calls() { + mmcli -m any --voice-list-calls | \ + awk '$1=$1' | \ + grep -v terminated | \ + grep -v "No calls were found" | while read -r line; do + CALLID="$(printf "%s\n" "$line" | awk '$1=$1' | cut -d" " -f1 | xargs basename)" + if [ -e "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.mutedring" ]; then + continue # we shallow muted calls + fi + printf "%s\n" "$line" + done +} + +incall_menu() { + if command -v xcallscreen >/dev/null 2>&1; then + ACTIVE_LINE="$(list_active_calls | head -n1)" + if [ -n "$ACTIVE_LINE" ]; then + CALLID="$(printf %s "$ACTIVE_LINE" | cut -d" " -f1 | xargs basename)" + NUMBER="$(vid_to_number "$CALLID" 2>/dev/null || true)" + case "$ACTIVE_LINE" in + *"(ringing-in)"*) + xcallscreen incoming "$CALLID" "$NUMBER" >/dev/null 2>&1 || true + ;; + *"(dialing)"*|*"(ringing-out)"*) + xcallscreen outgoing "$CALLID" "$NUMBER" >/dev/null 2>&1 || true + ;; + *) + xcallscreen active "$CALLID" "$NUMBER" >/dev/null 2>&1 || true + ;; + esac + else + xcallscreen >/dev/null 2>&1 || true + fi + return 0 + fi +} + +mute() { + CALLID="$1" + + # this signals that we muted this ring + touch "$XDG_RUNTIME_DIR/sxmo_calls/${CALLID}.mutedring" + sxmo_log "Invoking mute_ring hook" + sxmo_hook_mute_ring.sh + log_event "ring_mute" "$1" +} + +incoming_call_menu() { + NUMBER="$(vid_to_number "$1")" + NUMBER="$(sxmo_modem.sh cleanupnumber "$NUMBER")" + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen incoming "$1" "$NUMBER" >/dev/null 2>&1 || true + return 0 + fi +} + +killed() { + sxmo_dmenu.sh close +} +if [ "$1" = "incall_menu" ] || [ "$1" = "incoming_call_menu" ]; then + trap 'killed' TERM INT +fi + +"$@" diff --git a/overlay/rootfs/usr/bin/sxmo_modemdial.sh b/overlay/rootfs/usr/bin/sxmo_modemdial.sh index 57a6e12..86b0e6d 100755 --- a/overlay/rootfs/usr/bin/sxmo_modemdial.sh +++ b/overlay/rootfs/usr/bin/sxmo_modemdial.sh @@ -28,12 +28,18 @@ EOF fi sxmo_log "Attempting to dial: $NUMBER" + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen outgoing "-" "$NUMBER" >/dev/null 2>&1 || true + fi if ! CALLID="$( mmcli -m any --voice-create-call "number=$NUMBER" | grep -Eo "Call/[0-9]+" | grep -oE "[0-9]+" )"; then + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen ended >/dev/null 2>&1 || true + fi sxmo_notify_user.sh --urgency=critical "We failed to initiate call" return 1 fi @@ -41,17 +47,26 @@ EOF # cleanup all dangling event files, ignore errors rm "$XDG_RUNTIME_DIR/sxmo_calls/$CALLID."* 2>/dev/null || true sxmo_log "Starting call with CALLID: $CALLID" + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen outgoing "$CALLID" "$NUMBER" >/dev/null 2>&1 || true + fi sxmo_log "Invoking dial hook" sxmo_hook_dial.sh if ! sxmo_modemaudio.sh setup_audio; then + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen ended >/dev/null 2>&1 || true + fi sxmo_notify_user.sh --urgency=critical "We failed to setup call audio" mmcli -m any --voice-delete-call="$CALLID" return 1 fi if ! sxmo_modemcall.sh pickup "$CALLID"; then + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen ended >/dev/null 2>&1 || true + fi sxmo_modemaudio.sh reset_audio mmcli -m any --voice-delete-call="$CALLID" return 1 diff --git a/overlay/rootfs/usr/bin/xcallscreen b/overlay/rootfs/usr/bin/xcallscreen new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xcallscreen @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so index 7e87009..c23547b 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so and b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so new file mode 100755 index 0000000..975d0b3 Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/camera.so b/overlay/rootfs/usr/lib/dwm/plugins/camera.so index c9d8508..ddf5732 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/camera.so and b/overlay/rootfs/usr/lib/dwm/plugins/camera.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so index ffb3df3..5adc323 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so and b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/dialer.so b/overlay/rootfs/usr/lib/dwm/plugins/dialer.so index 310c92c..c57740e 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/dialer.so and b/overlay/rootfs/usr/lib/dwm/plugins/dialer.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so b/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so index 8a9e98b..43dc745 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so and b/overlay/rootfs/usr/lib/dwm/plugins/dmenu.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/files.so b/overlay/rootfs/usr/lib/dwm/plugins/files.so index 232b28a..1d1c7a4 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/files.so and b/overlay/rootfs/usr/lib/dwm/plugins/files.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/sms.so b/overlay/rootfs/usr/lib/dwm/plugins/sms.so index 02fb22d..34a7b4c 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/sms.so and b/overlay/rootfs/usr/lib/dwm/plugins/sms.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/themes.so b/overlay/rootfs/usr/lib/dwm/plugins/themes.so index 48baa9a..c1f42f4 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/themes.so and b/overlay/rootfs/usr/lib/dwm/plugins/themes.so differ diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_after_call.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_after_call.sh index 8213e5f..1871100 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_after_call.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_after_call.sh @@ -3,4 +3,8 @@ # This script is executed after the last call finished +if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen ended >/dev/null 2>&1 || true +fi + sxmo_playerctl.sh resume_all diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_inputhandler.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_inputhandler.sh index 8246223..3ee80a2 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_inputhandler.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_inputhandler.sh @@ -401,8 +401,14 @@ case "$ACTION" in exit 0 ;; "enter_one") - if pidof -s sxmo_RepeatRing.sh >/dev/null; then - sxmo_hook_pickup.sh + if [ -e /tmp/sxmo_call_ringing ] || [ -e "$XDG_RUNTIME_DIR/sxmo_jobs/ringing" ] || pidof -s sxmo_RepeatRing.sh >/dev/null; then + if command -v xcallscreen >/dev/null 2>&1; then + xcallscreen accept + else + sxmo_hook_pickup.sh + fi + elif [ -e /tmp/sxmo_active_call ] && command -v xcallscreen >/dev/null 2>&1; then + xcallscreen else sxmo_appmenu.sh "$WMCLASS" "$WMNAME" fi diff --git a/prebuilt/sxmo-x11/usr/bin/dwm-plugin b/prebuilt/sxmo-x11/usr/bin/dwm-plugin index 81ac988..9e3f0a3 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dwm-plugin and b/prebuilt/sxmo-x11/usr/bin/dwm-plugin differ diff --git a/prebuilt/sxmo-x11/usr/bin/xcallscreen b/prebuilt/sxmo-x11/usr/bin/xcallscreen new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xcallscreen @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so index 7e87009..c23547b 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so new file mode 100755 index 0000000..975d0b3 Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so index c9d8508..ddf5732 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so index ffb3df3..5adc323 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so index 310c92c..c57740e 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so index 8a9e98b..43dc745 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dmenu.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so index 232b28a..1d1c7a4 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so index 02fb22d..34a7b4c 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so index 48baa9a..c1f42f4 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so differ diff --git a/scripts/build-sxmo-x11.sh b/scripts/build-sxmo-x11.sh index 549c06b..314372e 100755 --- a/scripts/build-sxmo-x11.sh +++ b/scripts/build-sxmo-x11.sh @@ -100,7 +100,7 @@ EOF dwm.c drw.c util.c $DWM_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm $STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm - echo "[6/6] Compiling DWM Plugins (xdrawer, dialer, calllog, contacts, sms, files, camera, themes) and dwm-plugin runner..." + echo "[6/6] Compiling DWM Plugins (xdrawer, dialer, calllog, callscreen, contacts, sms, files, camera, themes) and dwm-plugin runner..." mkdir -p /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins PLUG_INCS="$(pkgconf --cflags x11 xft fontconfig freetype2 pango pangoxft alsa libjpeg libpng)" PLUG_LIBS="$(pkgconf --libs x11 xft fontconfig freetype2 pango pangoxft alsa)" @@ -118,6 +118,10 @@ EOF /bananarchy/src/dwm-plugins/calllog_plugin.cpp $PLUG_LIBS \ -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so + $CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \ + /bananarchy/src/dwm-plugins/callscreen_plugin.cpp $PLUG_LIBS \ + -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so + $CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \ /bananarchy/src/dwm-plugins/contacts_plugin.cpp $PLUG_LIBS \ -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so @@ -142,6 +146,7 @@ EOF /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so \ + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so \ @@ -159,6 +164,7 @@ EOF ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/XDrawer ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xdialer ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcalllog + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcallscreen ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcontacts ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xsms ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xfiles diff --git a/src/dwm-plugins/calllog_plugin.cpp b/src/dwm-plugins/calllog_plugin.cpp index d5d8f29..93d316b 100644 --- a/src/dwm-plugins/calllog_plugin.cpp +++ b/src/dwm-plugins/calllog_plugin.cpp @@ -631,16 +631,20 @@ int calllog_plugin_init(const DwmHostApi* host) { int calllog_on_global_key(const DwmHostApi* host, KeySym keysym, unsigned int keycode, unsigned int state, int is_press) { + (void)host; if (!is_press || state != 0) { return 0; } if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { if (isCallRingingFast()) { - const char* const argv[] = {"sxmo_hook_pickup.sh", nullptr}; - if (host && host->spawn_command) { - host->spawn_command(argv); - } else { - runAsyncArgv("sxmo_hook_pickup.sh"); + if (!sendPluginCommand("callscreen", "ACCEPT")) { + runAsyncArgv("xcallscreen", "accept"); + } + return 1; + } + if (isCallActiveOrRingingFast()) { + if (!sendPluginCommand("callscreen", "OPEN")) { + runAsyncArgv("xcallscreen"); } return 1; } @@ -655,9 +659,14 @@ int calllog_on_idle_key(const DwmHostApi* host, KeySym keysym, } if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { if (isCallRingingFast()) { - const char* const argv[] = {"sxmo_hook_pickup.sh", nullptr}; - if (host->spawn_command) { - host->spawn_command(argv); + if (!sendPluginCommand("callscreen", "ACCEPT")) { + runAsyncArgv("xcallscreen", "accept"); + } + return 1; + } + if (isCallActiveOrRingingFast()) { + if (!sendPluginCommand("callscreen", "OPEN")) { + runAsyncArgv("xcallscreen"); } return 1; } diff --git a/src/dwm-plugins/callscreen_plugin.cpp b/src/dwm-plugins/callscreen_plugin.cpp new file mode 100644 index 0000000..16acc05 --- /dev/null +++ b/src/dwm-plugins/callscreen_plugin.cpp @@ -0,0 +1,1728 @@ +// SPDX-License-Identifier: GPL-2.0-only +// CallScreen DWM Plugin - Unified X11 Incoming & In-Call UI for Bananarchy (240x320) + +#include +#include +#include +#include +#include +#include +#ifndef _EVDEVK +#define _EVDEVK(_v) (0x10081000 + (_v)) +#endif +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../sxmo-dwm/dwm_plugin.h" +#include "phone_data.h" + +namespace { + +using namespace bananarchy; + +volatile sig_atomic_t gRunning = 1; + +void handleSignal(int) { + gRunning = 0; +} + +class AlsaCallControl { + public: + AlsaCallControl() { + if (snd_ctl_open(&ctl_, "hw:0", 0) < 0) { + ctl_ = nullptr; + } + } + + ~AlsaCallControl() { + if (ctl_) { + snd_ctl_close(ctl_); + } + } + + long getInteger(const char* name, long fallback = 0) const { + if (!ctl_) { + return fallback; + } + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + if (snd_ctl_elem_read(ctl_, val) < 0) { + return fallback; + } + return snd_ctl_elem_value_get_integer(val, 0); + } + + bool setInteger(const char* name, long v) { + if (!ctl_) { + return false; + } + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + snd_ctl_elem_value_set_integer(val, 0, v); + return snd_ctl_elem_write(ctl_, val) >= 0; + } + + bool getBoolean(const char* name, bool fallback = true) const { + if (!ctl_) { + return fallback; + } + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + if (snd_ctl_elem_read(ctl_, val) < 0) { + return fallback; + } + return snd_ctl_elem_value_get_boolean(val, 0) != 0; + } + + bool setBoolean(const char* name, bool v) { + if (!ctl_) { + return false; + } + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + snd_ctl_elem_value_set_boolean(val, 0, v ? 1 : 0); + return snd_ctl_elem_write(ctl_, val) >= 0; + } + + unsigned int getEnum(const char* name, unsigned int fallback = 0) const { + if (!ctl_) { + return fallback; + } + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + if (snd_ctl_elem_read(ctl_, val) < 0) { + return fallback; + } + return snd_ctl_elem_value_get_enumerated(val, 0); + } + + bool setEnum(const char* name, unsigned int v) { + if (!ctl_) { + return false; + } + snd_ctl_elem_id_t* id = nullptr; + snd_ctl_elem_value_t* val = nullptr; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_value_set_id(val, id); + snd_ctl_elem_value_set_enumerated(val, 0, v); + return snd_ctl_elem_write(ctl_, val) >= 0; + } + + private: + snd_ctl_t* ctl_ = nullptr; +}; + +enum class CallState { + Idle = 0, + Incoming = 1, + Outgoing = 2, + Active = 3, + Ended = 4, +}; + +std::string formatTimerSec(int totalSec) { + if (totalSec < 0) { + totalSec = 0; + } + char buf[32]; + if (totalSec >= 3600) { + std::snprintf(buf, sizeof(buf), "%d:%02d:%02d", + totalSec / 3600, (totalSec % 3600) / 60, totalSec % 60); + } else { + std::snprintf(buf, sizeof(buf), "%02d:%02d", + totalSec / 60, totalSec % 60); + } + return buf; +} + +std::string sanitizeForFilename(const std::string& raw) { + std::string out; + for (unsigned char c : raw) { + if (std::isalnum(c) || c == '+' || c == '-' || c == '_') { + out.push_back(static_cast(c)); + } else if (c == ' ') { + out.push_back('_'); + } + } + if (out.empty()) { + return "unknown"; + } + if (out.size() > 20) { + out = out.substr(0, 20); + } + return out; +} + +std::string getRecordingsDir() { + const char* home = std::getenv("HOME"); + std::string dir = std::string(home && home[0] ? home : "/home/user") + "/Recordings"; + ::mkdir(dir.c_str(), 0755); + return dir; +} + +void wakeDisplayHardware(Display* dpy) { + int saved = readIntFile("/tmp/sxmo_saved_brightness", -1); + if (saved <= 1) { + saved = readIntFile("/sys/class/backlight/backlight/max_brightness", 14); + if (saved <= 1) { + saved = 14; + } + } + writeIntFile("/sys/class/backlight/backlight/bl_power", 0); + writeIntFile("/sys/class/backlight/backlight/brightness", saved); + writeIntFile("/sys/class/leds/white:kbd_backlight/brightness", 255); + if (dpy) { + XForceScreenSaver(dpy, ScreenSaverReset); + } +} + +int callscreen_run_main(int argc, char** argv) { + gRunning = 1; + bool isPreload = false; + std::string initCmd; + + for (int i = 1; i < argc; ++i) { + if (!argv[i] || !argv[i][0]) { + continue; + } + if (std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } else { + if (!initCmd.empty()) { + initCmd.push_back(' '); + } + initCmd += argv[i]; + } + } + + auto buildFifoCmdFromArgs = [&]() -> std::string { + if (isPreload) { + return "NOP"; + } + if (argc >= 2 && argv[1]) { + std::string v = toLowerStr(argv[1]); + std::string a1 = (argc >= 3 && argv[2]) ? argv[2] : "-"; + std::string a2 = (argc >= 4 && argv[3]) ? argv[3] : ""; + if (v == "incoming") { + return "INCOMING " + a1 + (a2.empty() ? "" : (" " + a2)); + } else if (v == "outgoing") { + return "OUTGOING " + a1 + (a2.empty() ? "" : (" " + a2)); + } else if (v == "active") { + return "ACTIVE " + a1 + (a2.empty() ? "" : (" " + a2)); + } else if (v == "accept" || v == "pickup") { + return "ACCEPT"; + } else if (v == "hangup" || v == "reject") { + return "HANGUP"; + } else if (v == "mute" || v == "mute_ring" || v == "ignore") { + return "MUTE_RING"; + } else if (v == "ended" || v == "end") { + return a1 != "-" ? ("ENDED " + a1) : "ENDED"; + } else if (v == "close") { + return "CLOSE"; + } else if (v == "record" || v == "toggle_rec") { + return "TOGGLE_REC"; + } else if (v == "mic" || v == "toggle_mic") { + return "TOGGLE_MIC"; + } + } + return initCmd.empty() ? "OPEN" : ("OPEN " + initCmd); + }; + + std::string forwardedCmd = buildFifoCmdFromArgs(); + if (sendPluginCommand("callscreen", forwardedCmd)) { + return 0; + } + + std::string daemonPidPath = getPluginDaemonPidPath("callscreen"); + std::string activePidPath = getRuntimeFile("xcallscreen.pid"); + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("callscreen"); + + struct sigaction sa {}; + sa.sa_handler = handleSignal; + sigemptyset(&sa.sa_mask); + sigaction(SIGTERM, &sa, nullptr); + sigaction(SIGINT, &sa, nullptr); + + struct sigaction saChld {}; + saChld.sa_handler = SIG_DFL; + sigemptyset(&saChld.sa_mask); + saChld.sa_flags = SA_NOCLDSTOP; + sigaction(SIGCHLD, &saChld, 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); + + XSetWindowAttributes swa{}; + swa.override_redirect = True; + swa.background_pixel = 0x222222; + swa.border_pixel = 0x005577; + swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | + StructureNotifyMask | VisibilityChangeMask; + + Window win = XCreateWindow( + dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, + DefaultDepth(dpy, screen), CopyFromParent, visual, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); + + XClassHint ch{}; + char resName[] = "xcallscreen"; + char resClass[] = "XCallScreen"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Call Screen"); + + Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, DefaultDepth(dpy, screen)); + GC gc = XCreateGC(dpy, pix, 0, nullptr); + XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); + + PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen); + PangoContext* pangoCtx = pango_font_map_create_context(fontMap); + + PangoLayout* layoutNum = pango_layout_new(pangoCtx); + PangoLayout* layoutMain = pango_layout_new(pangoCtx); + PangoLayout* layoutSmall = pango_layout_new(pangoCtx); + PangoLayout* layoutIcon = pango_layout_new(pangoCtx); + + PluginTheme theme; + theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, + layoutNum, win); + XftColor& colWhite = theme.colWhite; + XftColor& colLight = theme.colLight; + XftColor& colMuted = theme.colMuted; + XftColor& colYellow = theme.colYellow; + XftColor& colGreen = theme.colGreen; + XftColor& colRed = theme.colRed; + XftColor& colBlue = theme.colBlue; + + ContactBook book; + FileStamp contactsStamp; + bool contactsLoadedOnce = false; + + auto reloadContactsIfNeeded = [&]() { + if (contactsStamp.checkAndUpdate(getContactsFilePath()) || !contactsLoadedOnce) { + contactsLoadedOnce = true; + book.load(); + } + }; + reloadContactsIfNeeded(); + + AlsaCallControl alsa; + unsigned int savedMicMux = 2; + + CallState callState = CallState::Idle; + std::string callId; + std::string callNumber; + std::string contactName; + std::string endReason = "CALL ENDED"; + std::string statusNotice; + int64_t statusNoticeUntilMs = 0; + std::string dtmfSent; + bool dtmfMode = false; + bool ringMuted = false; + bool micMuted = false; + bool speakerOn = false; + int callVolumePct = 80; + int selectedRow = 0; + + int64_t callStartMs = 0; + int64_t endedAtMs = 0; + int lastFinalDurationSec = 0; + int64_t lastModemPollMs = 0; + + pid_t recordPid = -1; + int64_t recordStartMs = 0; + std::string recordFilePath; + std::string recordFileName; + + bool visible = false; + int64_t openStartMs = 0; + int64_t startupIgnoreUntilMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + auto refreshHardwareAudioState = [&]() { + unsigned int dec1 = alsa.getEnum("DEC1 MUX", 2); + if (dec1 > 0) { + savedMicMux = dec1; + writeIntFile("/tmp/xdrawer_mic_mux", static_cast(savedMicMux)); + micMuted = false; + } else { + savedMicMux = static_cast(std::max(1, readIntFile("/tmp/xdrawer_mic_mux", 2))); + micMuted = true; + } + bool spkSwitch = alsa.getBoolean("SPK DAC Switch", false); + bool earSwitch = alsa.getBoolean("EAR_S", true); + speakerOn = (spkSwitch && !earSwitch); + + long rawVol = speakerOn ? alsa.getInteger("RX3 Digital Volume", 68) + : alsa.getInteger("RX1 Digital Volume", 68); + if (rawVol <= 0) { + rawVol = alsa.getInteger("RX1 Digital Volume", 68); + } + callVolumePct = std::clamp(static_cast(std::lround((rawVol * 100.0) / 84.0)), 0, 100); + callVolumePct = (callVolumePct / 5) * 5; + if (callVolumePct == 0) { + callVolumePct = 75; + } + }; + + auto setMicMuted = [&](bool mute) { + micMuted = mute; + alsa.setEnum("DEC1 MUX", mute ? 0 : savedMicMux); + alsa.setInteger("ADC3 Volume", mute ? 0 : 8); + alsa.setBoolean("VoiceMMode1 Capture Mixer TERT_MI2S_TX", !mute); + runAsyncCmd(mute ? "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1 >/dev/null 2>&1" + : "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0 >/dev/null 2>&1"); + statusNotice = mute ? "Microphone muted" : "Microphone unmuted"; + statusNoticeUntilMs = nowMs() + 2000; + }; + + auto setSpeakerRoute = [&](bool toSpeaker) { + speakerOn = toSpeaker; + long hwVol = std::lround((callVolumePct * 84.0) / 100.0); + if (toSpeaker) { + alsa.setBoolean("EAR_S", false); + alsa.setEnum("RX1 MIX1 INP1", 0); + alsa.setBoolean("SPK DAC Switch", true); + alsa.setEnum("RX3 MIX1 INP1", 1); + alsa.setInteger("RX3 Digital Volume", hwVol); + runAsyncCmd("sxmo_audio.sh device set Speaker >/dev/null 2>&1"); + statusNotice = "Audio routed to Speaker"; + } else { + alsa.setBoolean("SPK DAC Switch", false); + alsa.setEnum("RX3 MIX1 INP1", 0); + alsa.setEnum("RX1 MIX1 INP1", 1); + alsa.setEnum("RDAC2 MUX", 1); + alsa.setBoolean("EAR_S", true); + alsa.setInteger("RX1 Digital Volume", hwVol); + runAsyncCmd("sxmo_audio.sh device set Earpiece >/dev/null 2>&1"); + statusNotice = "Audio routed to Earpiece"; + } + statusNoticeUntilMs = nowMs() + 2000; + }; + + auto adjustVolume = [&](int deltaPct) { + callVolumePct = std::clamp(callVolumePct + deltaPct, 5, 100); + long hwVol = std::lround((callVolumePct * 84.0) / 100.0); + alsa.setInteger("RX1 Digital Volume", hwVol); + if (speakerOn) { + alsa.setInteger("RX3 Digital Volume", hwVol); + } + char cmd[128]; + std::snprintf(cmd, sizeof(cmd), + "wpctl set-volume @DEFAULT_AUDIO_SINK@ %d%% >/dev/null 2>&1", + callVolumePct); + runAsyncCmd(cmd); + }; + + auto checkRecordingAlive = [&]() -> bool { + if (recordPid <= 0) { + return false; + } + int status = 0; + pid_t r = ::waitpid(recordPid, &status, WNOHANG); + if (r == recordPid || (r < 0 && ::kill(recordPid, 0) != 0)) { + recordPid = -1; + return false; + } + return true; + }; + + auto stopRecording = [&]() { + if (recordPid <= 0) { + return; + } + ::kill(recordPid, SIGINT); + for (int i = 0; i < 15; ++i) { + int status = 0; + if (::waitpid(recordPid, &status, WNOHANG) == recordPid) { + recordPid = -1; + break; + } + usleep(10000); + } + if (recordPid > 0) { + ::kill(recordPid, SIGTERM); + (void)::waitpid(recordPid, nullptr, WNOHANG); + recordPid = -1; + } + if (!recordFileName.empty()) { + statusNotice = "Saved: " + recordFileName; + statusNoticeUntilMs = nowMs() + 3500; + } + }; + + auto startRecording = [&]() -> bool { + if (checkRecordingAlive()) { + return true; + } + std::string dir = getRecordingsDir(); + time_t nowSec = ::time(nullptr); + struct tm tmNow {}; + localtime_r(&nowSec, &tmNow); + char tsBuf[32]; + std::strftime(tsBuf, sizeof(tsBuf), "%Y%m%d_%H%M%S", &tmNow); + + std::string tag = sanitizeForFilename( + !contactName.empty() ? contactName : (!callNumber.empty() ? callNumber : "voice")); + recordFileName = "call_" + tag + "_" + tsBuf + ".wav"; + recordFilePath = dir + "/" + recordFileName; + + // Ensure MultiMedia2 capture mixer is enabled for PipeWire recording + alsa.setBoolean("MultiMedia2 Mixer TERT_MI2S_TX", true); + + pid_t pid = ::fork(); + if (pid < 0) { + return false; + } + if (pid == 0) { + ::setsid(); + int nullfd = ::open("/dev/null", O_RDWR); + if (nullfd >= 0) { + ::dup2(nullfd, STDIN_FILENO); + ::dup2(nullfd, STDOUT_FILENO); + ::dup2(nullfd, STDERR_FILENO); + if (nullfd > 2) { + ::close(nullfd); + } + } + execlp("pw-record", "pw-record", "--rate", "16000", "--channels", "1", + recordFilePath.c_str(), static_cast(nullptr)); + execlp("parecord", "parecord", "--rate=16000", "--channels=1", + "--file-format=wav", recordFilePath.c_str(), static_cast(nullptr)); + execlp("arecord", "arecord", "-f", "S16_LE", "-r", "16000", "-c", "1", + recordFilePath.c_str(), static_cast(nullptr)); + _exit(1); + } + recordPid = pid; + recordStartMs = nowMs(); + statusNotice = "Recording to " + recordFileName; + statusNoticeUntilMs = nowMs() + 2500; + return true; + }; + + auto toggleRecording = [&]() { + if (checkRecordingAlive()) { + stopRecording(); + } else { + startRecording(); + } + }; + + auto updateCallerDetails = [&](const std::string& rawId, const std::string& rawNum) { + reloadContactsIfNeeded(); + if (!rawId.empty() && rawId != "-") { + callId = trimStr(rawId); + } else if (callId.empty()) { + callId = "-"; + } + if (!rawNum.empty() && rawNum != "-") { + callNumber = trimStr(rawNum); + } + if (!callNumber.empty()) { + contactName = book.resolveName(callNumber); + } else { + contactName.clear(); + } + }; + + auto writeActiveStateFiles = [&]() { + if (callState == CallState::Incoming) { + writeIntFile("/tmp/sxmo_call_ringing", 1); + writeIntFile("/tmp/sxmo_active_call", 1); + } else if (callState == CallState::Outgoing || callState == CallState::Active) { + ::unlink("/tmp/sxmo_call_ringing"); + writeIntFile("/tmp/sxmo_active_call", 2); + } else { + ::unlink("/tmp/sxmo_call_ringing"); + ::unlink("/tmp/sxmo_active_call"); + } + }; + + auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, + const std::string& text, int maxW = -1) { + pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); + if (maxW > 0) { + pango_layout_set_width(layout, maxW * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } else { + pango_layout_set_width(layout, -1); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE); + } + PangoRectangle ink{}, logical{}; + pango_layout_get_pixel_extents(layout, &ink, &logical); + int ty = y + (h - logical.height) / 2; + pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, + ty * PANGO_SCALE); + }; + + auto pangoWidth = [&](PangoLayout* layout, const std::string& text) -> int { + pango_layout_set_width(layout, -1); + pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); + PangoRectangle ink{}, logical{}; + pango_layout_get_pixel_extents(layout, &ink, &logical); + return logical.width; + }; + + constexpr int kHeaderH = 116; + constexpr int kSoftbarH = 26; + constexpr int kBodyY = kHeaderH; + constexpr int kBodyH = kWinHeight - kHeaderH - kSoftbarH; // 154px + + auto render = [&]() { + int64_t now = nowMs(); + bool isRec = checkRecordingAlive(); + + XSetForeground(dpy, gc, theme.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // 1. Top Caller Card (0..kHeaderH) + XSetForeground(dpy, gc, theme.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH); + + // Top Status Strip (0..26) + const char* stateIcon = kIconPhone; + std::string stateBadge = "READY"; + XftColor* stateCol = &colBlue; + unsigned long stateAccentPx = theme.spec.accent; + + if (callState == CallState::Incoming) { + stateIcon = ringMuted ? kIconSoundOff : kIconInCall; + stateBadge = ringMuted ? "RING SILENCED" : "INCOMING CALL"; + stateCol = ringMuted ? &colYellow : &colGreen; + stateAccentPx = theme.spec.accent; + } else if (callState == CallState::Outgoing) { + stateIcon = kIconOutCall; + stateBadge = "DIALING..."; + stateCol = &colYellow; + stateAccentPx = theme.spec.accent; + } else if (callState == CallState::Active) { + stateIcon = kIconPhone; + stateBadge = "IN CALL"; + stateCol = &colGreen; + stateAccentPx = theme.spec.accent; + } else if (callState == CallState::Ended) { + stateIcon = kIconMissedCall; + stateBadge = endReason; + stateCol = &colRed; + stateAccentPx = theme.spec.border; + } + + drawPango(layoutIcon, stateCol, 8, 3, 22, stateIcon); + drawPango(layoutSmall, stateCol, 28, 3, 22, stateBadge, 138); + + int elapsedSec = 0; + if (callState == CallState::Ended) { + elapsedSec = lastFinalDurationSec; + } else if (callStartMs > 0 && callState != CallState::Idle) { + elapsedSec = static_cast((now - callStartMs) / 1000); + } + if (callState != CallState::Idle || elapsedSec > 0) { + std::string timerStr = formatTimerSec(elapsedSec); + int tw = pangoWidth(layoutMain, timerStr); + drawPango(layoutMain, + (callState == CallState::Active) ? &colWhite : stateCol, + kWinWidth - tw - 8, 2, 24, timerStr); + } + + // Caller Avatar Box + Name + Number (28..88) + XSetForeground(dpy, gc, theme.spec.bgAltOdd); + XFillRectangle(dpy, pix, gc, 8, 32, 42, 42); + XSetForeground(dpy, gc, stateAccentPx); + XDrawRectangle(dpy, pix, gc, 8, 32, 41, 41); + + const char* avatarIcon = contactName.empty() ? kIconPhone : kIconUser; + int aw = pangoWidth(layoutIcon, avatarIcon); + drawPango(layoutIcon, stateCol, 8 + (42 - aw) / 2, 32, 42, avatarIcon); + + std::string primaryTitle; + std::string secondarySub; + if (!contactName.empty()) { + primaryTitle = contactName; + secondarySub = callNumber.empty() ? "Saved Contact" : callNumber; + } else if (!callNumber.empty()) { + primaryTitle = callNumber; + secondarySub = (callState == CallState::Idle) ? "Ready" : "Unknown Caller"; + } else { + primaryTitle = "Voice Call"; + secondarySub = (callState == CallState::Idle) ? "No active call" : "Private Number"; + } + + drawPango(layoutNum, &colWhite, 58, 30, 26, primaryTitle, 174); + drawPango(layoutMain, &colYellow, 58, 56, 22, secondarySub, 174); + + // Sub-header Banner (88..114) + XSetForeground(dpy, gc, theme.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 0, 88, kWinWidth, 26); + + if (isRec) { + int recSec = static_cast((now - recordStartMs) / 1000); + std::string recStr = "REC " + formatTimerSec(recSec); + drawPango(layoutIcon, &colRed, 8, 88, 26, kIconRecord); + drawPango(layoutSmall, &colRed, 26, 88, 26, recStr); + int fnW = pangoWidth(layoutSmall, recordFileName); + drawPango(layoutSmall, &colLight, std::max(96, kWinWidth - fnW - 8), 88, 26, + recordFileName, 136); + } else if (now < statusNoticeUntilMs && !statusNotice.empty()) { + drawPango(layoutSmall, &colGreen, 8, 88, 26, statusNotice, 224); + } else if (dtmfMode) { + std::string dtmfLabel = "DTMF (0-9/*/#): " + (dtmfSent.empty() ? "_" : dtmfSent); + drawPango(layoutSmall, &colYellow, 8, 88, 26, dtmfLabel, 224); + } else if (callState == CallState::Incoming) { + drawPango(layoutSmall, &colLight, 8, 88, 26, + "[Call] Answer [*] Mute [End] Reject", 224); + } else if (callState == CallState::Ended) { + drawPango(layoutSmall, &colMuted, 8, 88, 26, + recordFileName.empty() ? "Call finished" : ("Saved: " + recordFileName), + 224); + } else { + drawPango(layoutSmall, &colMuted, 8, 88, 26, + "[1] Mic [2] Rec [3] Route [0] DTMF", 224); + } + + XSetForeground(dpy, gc, stateAccentPx); + XFillRectangle(dpy, pix, gc, 0, kHeaderH - 2, kWinWidth, 2); + + // 2. Body Area (kBodyY..kBodyY+kBodyH) + if (callState == CallState::Incoming) { + struct IncomingAction { + const char* icon; + const char* title; + const char* hint; + const char* badge; + XftColor* color; + }; + IncomingAction actions[3] = { + {kIconPhone, "Answer Call", "Green Call or OK key", "ACCEPT", &colGreen}, + {kIconSoundOff, ringMuted ? "Ringtone Muted" : "Silence Ring", + "Press * or 0 key", ringMuted ? "MUTED" : "SILENCE", &colYellow}, + {kIconMissedCall, "Reject Call", "Red End or Right Soft", "REJECT", &colRed}, + }; + constexpr int kIncRows = 3; + for (int i = 0; i < kIncRows; ++i) { + int ry = kBodyY + (i * kBodyH) / kIncRows; + int nextRy = kBodyY + ((i + 1) * kBodyH) / kIncRows; + int rowH = nextRy - ry; + bool isSel = (selectedRow == i); + + XSetForeground(dpy, gc, + isSel ? theme.spec.selBg + : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); + if (isSel) { + XSetForeground(dpy, gc, theme.spec.accent); + XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); + } + + int bw = pangoWidth(layoutSmall, actions[i].badge); + int textMaxW = std::max(80, kWinWidth - bw - 48); + drawPango(layoutIcon, actions[i].color, 10, ry, rowH, actions[i].icon); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry + 4, 22, + actions[i].title, textMaxW); + drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 34, ry + 25, 18, + actions[i].hint, textMaxW); + drawPango(layoutSmall, actions[i].color, kWinWidth - bw - 8, ry, rowH, + actions[i].badge); + } + } else { + constexpr int kCtrlRows = 4; + for (int i = 0; i < kCtrlRows; ++i) { + int ry = kBodyY + (i * kBodyH) / kCtrlRows; + int nextRy = kBodyY + ((i + 1) * kBodyH) / kCtrlRows; + int rowH = nextRy - ry; + bool isSel = (selectedRow == i); + + XSetForeground(dpy, gc, + isSel ? theme.spec.selBg + : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); + if (isSel) { + XSetForeground(dpy, gc, theme.spec.accent); + XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); + } + + if (i == 0) { + const char* ic = micMuted ? kIconMicOff : kIconMicOn; + XftColor* icCol = micMuted ? &colRed : &colGreen; + std::string valStr = micMuted ? "MUTED" : "ON"; + int vw = pangoWidth(layoutMain, valStr); + drawPango(layoutIcon, icCol, 10, ry, rowH, ic); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH, + "[1] Microphone", std::max(80, kWinWidth - vw - 48)); + drawPango(layoutMain, icCol, kWinWidth - vw - 8, ry, rowH, valStr); + } else if (i == 1) { + XftColor* icCol = isRec ? &colRed : &colMuted; + std::string valStr = isRec + ? ("REC " + formatTimerSec(static_cast((now - recordStartMs) / 1000))) + : "OFF"; + int vw = pangoWidth(layoutMain, valStr); + drawPango(layoutIcon, icCol, 10, ry, rowH, kIconRecord); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH, + "[2] Record", std::max(80, kWinWidth - vw - 44)); + drawPango(layoutMain, isRec ? &colRed : &colMuted, + kWinWidth - vw - 8, ry, rowH, valStr); + } else if (i == 2) { + const char* ic = speakerOn ? kIconSpeaker : kIconEarpiece; + XftColor* icCol = speakerOn ? &colYellow : &colBlue; + std::string valStr = speakerOn ? "SPEAKER" : "EARPIECE"; + int vw = pangoWidth(layoutMain, valStr); + drawPango(layoutIcon, icCol, 10, ry, rowH, ic); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH, + "[3] Audio Out", std::max(80, kWinWidth - vw - 48)); + drawPango(layoutMain, icCol, kWinWidth - vw - 8, ry, rowH, valStr); + } else if (i == 3) { + drawPango(layoutIcon, &colLight, 10, ry, rowH, kIconSpeaker); + drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH, + "Volume", 70); + + int barX = 104; + int barW = 82; + int barH = 10; + int barY = ry + (rowH - barH) / 2; + XSetForeground(dpy, gc, theme.spec.bgMain); + XFillRectangle(dpy, pix, gc, barX, barY, barW, barH); + XSetForeground(dpy, gc, theme.spec.border); + XDrawRectangle(dpy, pix, gc, barX, barY, barW - 1, barH - 1); + + int fillW = ((barW - 2) * std::clamp(callVolumePct, 0, 100)) / 100; + if (fillW > 0) { + XSetForeground(dpy, gc, isSel ? theme.spec.accent : theme.spec.selBg); + XFillRectangle(dpy, pix, gc, barX + 1, barY + 1, fillW, barH - 2); + } + + char pctBuf[16]; + std::snprintf(pctBuf, sizeof(pctBuf), "%d%%", callVolumePct); + int pw = pangoWidth(layoutSmall, pctBuf); + drawPango(layoutSmall, isSel ? &colYellow : &colLight, + kWinWidth - pw - 8, ry, rowH, pctBuf); + } + } + } + + // 3. Softkey Footer Bar + int sbY = kWinHeight - kSoftbarH; + XSetForeground(dpy, gc, theme.spec.bgFooter); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH); + XSetForeground(dpy, gc, theme.spec.border); + XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1); + + std::string leftLabel; + std::string centerLabel; + std::string rightLabel; + XftColor* leftCol = &colLight; + XftColor* centerCol = &colGreen; + XftColor* rightCol = &colRed; + + if (callState == CallState::Incoming) { + leftLabel = "Accept"; + centerLabel = "ANSWER"; + rightLabel = "Reject"; + leftCol = &colGreen; + centerCol = &colGreen; + rightCol = &colRed; + } else if (callState == CallState::Outgoing || callState == CallState::Active) { + leftLabel = dtmfMode ? "Controls" : "DTMF"; + centerLabel = dtmfMode ? "TONE" : "TOGGLE"; + rightLabel = "Hangup"; + leftCol = dtmfMode ? &colYellow : &colLight; + centerCol = &colYellow; + rightCol = &colRed; + } else if (callState == CallState::Ended) { + leftLabel = "Call Log"; + centerLabel = "CLOSE"; + rightLabel = "Close"; + leftCol = &colLight; + centerCol = &colLight; + rightCol = &colLight; + } else { + leftLabel = "Dialer"; + centerLabel = "TOGGLE"; + rightLabel = "Close"; + leftCol = &colLight; + centerCol = &colGreen; + rightCol = &colLight; + } + + drawPango(layoutMain, leftCol, 6, sbY + 1, kSoftbarH - 1, leftLabel); + int cw = pangoWidth(layoutMain, centerLabel); + drawPango(layoutMain, centerCol, (kWinWidth - cw) / 2, sbY + 1, + kSoftbarH - 1, centerLabel); + int rw = pangoWidth(layoutMain, rightLabel); + drawPango(layoutMain, rightCol, kWinWidth - rw - 6, sbY + 1, + kSoftbarH - 1, rightLabel); + + if (visible) { + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + } + }; + + // Pre-warm glyph cache into offscreen Pixmap at startup + render(); + drawPango(layoutIcon, &colGreen, 0, 0, 20, + std::string(kIconPhone) + kIconInCall + kIconOutCall + kIconMissedCall + + kIconMicOn + kIconMicOff + kIconSpeaker + kIconEarpiece + kIconRecord + + kIconSoundOff + kIconUser); + render(); + XSync(dpy, False); + + auto showWindow = [&]() { + closeOtherPlugins("callscreen"); + wakeDisplayHardware(dpy); + theme.checkAndReload(dpy, screen, visual, cmap, layoutMain, layoutSmall, + layoutIcon, layoutNum, win); + reloadContactsIfNeeded(); + refreshHardwareAudioState(); + + if (!visible) { + openStartMs = nowMs(); + startupIgnoreUntilMs = openStartMs + 70; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + softkeyDeadlineMs = 0; + visible = true; + XMapRaised(dpy, win); + render(); + for (int i = 0; i < 20; ++i) { + if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == + GrabSuccess) { + break; + } + usleep(2000); + } + XFlush(dpy); + writeIntFile(activePidPath, static_cast(::getpid())); + } else { + XMapRaised(dpy, win); + render(); + } + }; + + auto hideWindow = [&](bool drainSoftkeys) { + if (!visible) { + return; + } + int xfd = ConnectionNumber(dpy); + if (drainSoftkeys) { + int64_t drainDeadline = nowMs() + 180; + while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) { + while (XPending(dpy)) { + XEvent ev; + XNextEvent(dpy, &ev); + } + struct pollfd pfd {}; + pfd.fd = xfd; + pfd.events = POLLIN; + ::poll(&pfd, 1, 10); + } + writeSoftkeySuppress(nowMs() + 300); + } + XUngrabKeyboard(dpy, CurrentTime); + XUnmapWindow(dpy, win); + XFlush(dpy); + ::unlink(activePidPath.c_str()); + visible = false; + softkeyPending = 0; + softkeyDeadlineMs = 0; + }; + + auto resolveActiveModemCallId = [&]() -> std::string { + if (!callId.empty() && callId != "-" && callId != "sim" && callId != "test") { + return callId; + } + std::string callsDir = getRuntimeFile("sxmo_calls"); + DIR* d = ::opendir(callsDir.c_str()); + if (d) { + struct dirent* ent; + while ((ent = ::readdir(d)) != nullptr) { + std::string name = ent->d_name; + size_t dot = name.find('.'); + if (dot != std::string::npos && dot > 0) { + std::string candidate = name.substr(0, dot); + bool allDigits = true; + for (char c : candidate) { + if (!std::isdigit(static_cast(c))) { + allDigits = false; + break; + } + } + if (allDigits) { + ::closedir(d); + callId = candidate; + return callId; + } + } + } + ::closedir(d); + } + return callId; + }; + + auto acceptIncomingCall = [&]() { + if (callState != CallState::Incoming) { + return; + } + std::string cid = resolveActiveModemCallId(); + callState = CallState::Active; + callStartMs = nowMs(); + ringMuted = false; + dtmfMode = false; + dtmfSent.clear(); + selectedRow = 0; + writeActiveStateFiles(); + setSpeakerRoute(false); + render(); + + if (!cid.empty() && cid != "-" && cid != "sim" && cid != "test") { + runAsyncArgv("sxmo_modemcall.sh", "pickup", cid.c_str()); + } else { + runAsyncCmd("sxmo_jobs.sh stop ringing >/dev/null 2>&1"); + } + }; + + auto silenceRing = [&]() { + if (callState != CallState::Incoming || ringMuted) { + return; + } + ringMuted = true; + std::string cid = resolveActiveModemCallId(); + if (!cid.empty() && cid != "-" && cid != "sim" && cid != "test") { + runAsyncArgv("sxmo_modemcall.sh", "mute", cid.c_str()); + } else { + runAsyncCmd("sxmo_jobs.sh stop ringing >/dev/null 2>&1"); + } + statusNotice = "Ringtone silenced"; + statusNoticeUntilMs = nowMs() + 2500; + render(); + }; + + auto transitionToEnded = [&](const std::string& reason) { + stopRecording(); + if (micMuted) { + setMicMuted(false); + } + if (callStartMs > 0 && + (callState == CallState::Active || callState == CallState::Outgoing)) { + lastFinalDurationSec = static_cast((nowMs() - callStartMs) / 1000); + } else { + lastFinalDurationSec = 0; + } + endReason = reason.empty() ? "CALL ENDED" : reason; + callState = CallState::Ended; + endedAtMs = nowMs(); + dtmfMode = false; + ringMuted = false; + writeActiveStateFiles(); + render(); + }; + + auto hangupCall = [&]() { + std::string cid = resolveActiveModemCallId(); + bool wasIncoming = (callState == CallState::Incoming); + if (!cid.empty() && cid != "-" && cid != "sim" && cid != "test") { + runAsyncArgv("sxmo_modemcall.sh", "hangup", cid.c_str()); + } else { + runAsyncCmd("sxmo_jobs.sh stop ringing >/dev/null 2>&1; " + "mmcli -m any --voice-hangup-all >/dev/null 2>&1"); + } + transitionToEnded(wasIncoming ? "CALL REJECTED" : "CALL ENDED"); + }; + + auto sendDtmfTone = [&](char ch) { + dtmfSent.push_back(ch); + if (dtmfSent.size() > 16) { + dtmfSent.erase(0, dtmfSent.size() - 16); + } + std::string cid = resolveActiveModemCallId(); + if (!cid.empty() && cid != "-" && cid != "sim" && cid != "test") { + char cmd[160]; + std::snprintf(cmd, sizeof(cmd), + "mmcli -m any -o '%s' --send-dtmf='%c' >/dev/null 2>&1", + cid.c_str(), ch); + runAsyncCmd(cmd); + } + render(); + }; + + auto syncModemCalls = [&]() { + if (callId == "sim" || callId == "test") { + return; + } + FILE* fp = ::popen("mmcli -m any --voice-list-calls -a 2>/dev/null", "r"); + if (!fp) { + return; + } + char line[512]; + std::string foundId; + std::string foundDir; + std::string foundState; + + while (std::fgets(line, sizeof(line), fp)) { + std::string s(line); + if (s.find("terminated") != std::string::npos || + s.find("No calls") != std::string::npos) { + continue; + } + size_t callPos = s.find("/Call/"); + if (callPos == std::string::npos) { + continue; + } + size_t idStart = callPos + 6; + size_t idEnd = idStart; + while (idEnd < s.size() && std::isdigit(static_cast(s[idEnd]))) { + ++idEnd; + } + if (idEnd == idStart) { + continue; + } + foundId = s.substr(idStart, idEnd - idStart); + if (s.find("incoming") != std::string::npos) { + foundDir = "incoming"; + } else if (s.find("outgoing") != std::string::npos) { + foundDir = "outgoing"; + } + size_t lp = s.find('('); + size_t rp = s.find(')', lp == std::string::npos ? 0 : lp); + if (lp != std::string::npos && rp != std::string::npos && rp > lp + 1) { + foundState = s.substr(lp + 1, rp - lp - 1); + } + break; + } + ::pclose(fp); + + if (!foundId.empty()) { + if (callId.empty() || callId == "-") { + callId = foundId; + } + if (callNumber.empty()) { + char numCmd[160]; + std::snprintf(numCmd, sizeof(numCmd), + "sxmo_modemcall.sh vid_to_number '%s' 2>/dev/null", + foundId.c_str()); + FILE* nfp = ::popen(numCmd, "r"); + if (nfp) { + char nbuf[128]; + if (std::fgets(nbuf, sizeof(nbuf), nfp)) { + updateCallerDetails(foundId, trimStr(nbuf)); + } + ::pclose(nfp); + } + } + if (foundState == "active" && callState != CallState::Active) { + callState = CallState::Active; + callStartMs = nowMs(); + ringMuted = false; + writeActiveStateFiles(); + render(); + } else if (foundState == "ringing-in" && callState == CallState::Idle) { + callState = CallState::Incoming; + callStartMs = nowMs(); + selectedRow = 0; + writeActiveStateFiles(); + render(); + } else if ((foundState == "dialing" || foundState == "ringing-out") && + callState == CallState::Idle) { + callState = CallState::Outgoing; + callStartMs = nowMs(); + selectedRow = 0; + writeActiveStateFiles(); + render(); + } + } else if (callState == CallState::Active || callState == CallState::Incoming || + (callState == CallState::Outgoing && nowMs() - callStartMs > 6000)) { + if (!isCallActiveOrRingingFast() || ::access("/tmp/sxmo_active_call", F_OK) == 0) { + // Verify sxmo_calls dir is empty before auto-ending + std::string callsDir = getRuntimeFile("sxmo_calls"); + bool anyFile = false; + DIR* d = ::opendir(callsDir.c_str()); + if (d) { + struct dirent* ent; + while ((ent = ::readdir(d)) != nullptr) { + if (ent->d_name[0] != '.' && ent->d_name[0] != '\0') { + anyFile = true; + break; + } + } + ::closedir(d); + } + if (!anyFile) { + transitionToEnded("CALL ENDED"); + } + } + } + }; + + auto handleFifoCommand = [&](const std::string& rawLine) { + std::string line = trimStr(rawLine); + if (line.empty() || line == "NOP") { + return; + } + if (line == "CLOSE") { + hideWindow(false); + return; + } + if (line == "RELOAD_THEME") { + theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, + layoutIcon, layoutNum, win); + render(); + return; + } + if (line == "ACCEPT") { + if (callState == CallState::Incoming) { + acceptIncomingCall(); + showWindow(); + } + return; + } + if (line == "HANGUP") { + if (callState == CallState::Incoming || callState == CallState::Outgoing || + callState == CallState::Active) { + hangupCall(); + } else { + hideWindow(false); + } + return; + } + if (line == "MUTE_RING") { + silenceRing(); + return; + } + if (line == "TOGGLE_MIC") { + setMicMuted(!micMuted); + render(); + return; + } + if (line == "TOGGLE_REC") { + toggleRecording(); + render(); + return; + } + if (line == "ENDED" || line.rfind("ENDED ", 0) == 0) { + std::string reason = (line.size() > 6) ? trimStr(line.substr(6)) : "CALL ENDED"; + if (callState != CallState::Idle) { + transitionToEnded(reason); + } else { + hideWindow(false); + } + return; + } + + // Parse INCOMING / OUTGOING / ACTIVE / OPEN + std::istringstream iss(line); + std::string verb, arg1, arg2; + iss >> verb >> arg1 >> arg2; + + if (verb == "OPEN" && !arg1.empty()) { + std::string low = toLowerStr(arg1); + if (low == "incoming" || low == "outgoing" || low == "active") { + verb = (low == "incoming") ? "INCOMING" : ((low == "outgoing") ? "OUTGOING" : "ACTIVE"); + arg1 = arg2.empty() ? "-" : arg2; + arg2.clear(); + iss >> arg2; + } + } + + if (verb == "INCOMING") { + callId.clear(); + callNumber.clear(); + contactName.clear(); + updateCallerDetails(arg1, arg2); + callState = CallState::Incoming; + callStartMs = nowMs(); + ringMuted = false; + dtmfMode = false; + dtmfSent.clear(); + selectedRow = 0; + writeActiveStateFiles(); + showWindow(); + } else if (verb == "OUTGOING") { + callId.clear(); + callNumber.clear(); + contactName.clear(); + updateCallerDetails(arg1, arg2); + callState = CallState::Outgoing; + callStartMs = nowMs(); + ringMuted = false; + dtmfMode = false; + dtmfSent.clear(); + selectedRow = 0; + writeActiveStateFiles(); + showWindow(); + } else if (verb == "ACTIVE") { + bool wasActive = (callState == CallState::Active); + updateCallerDetails(arg1, arg2); + callState = CallState::Active; + if (!wasActive) { + callStartMs = nowMs(); + } + ringMuted = false; + selectedRow = 0; + writeActiveStateFiles(); + showWindow(); + } else if (verb == "OPEN") { + if (!arg1.empty()) { + updateCallerDetails("-", arg1); + } + if (callState == CallState::Idle || callState == CallState::Ended) { + syncModemCalls(); + } + showWindow(); + } + }; + + if (!isPreload) { + handleFifoCommand(forwardedCmd); + } + + int xfd = ConnectionNumber(dpy); + + while (gRunning) { + for (const std::string& cmd : readFifoLines(fifoFd)) { + handleFifoCommand(cmd); + } + + int64_t now = nowMs(); + + // Auto-close Ended screen after 1300 ms + if (callState == CallState::Ended && endedAtMs > 0 && now - endedAtMs >= 1300) { + callState = CallState::Idle; + callId.clear(); + callNumber.clear(); + contactName.clear(); + endedAtMs = 0; + writeActiveStateFiles(); + hideWindow(false); + } + + // Periodically sync call state with ModemManager when a real call is in progress + if ((callState == CallState::Incoming || callState == CallState::Outgoing || + callState == CallState::Active) && + now - lastModemPollMs >= 1500) { + lastModemPollMs = now; + syncModemCalls(); + } + + if (visible && !initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + + while (XPending(dpy)) { + XEvent ev; + XNextEvent(dpy, &ev); + if (!visible) { + continue; + } + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + + if (ev.type == Expose) { + if (ev.xexpose.count == 0) { + render(); + } + } else if (ev.type == KeyRelease) { + KeySym sym = XLookupKeysym(&ev.xkey, 0); + if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || + sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || + ev.xkey.keycode == 135 || ev.xkey.keycode == 166) { + if (querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + } + } else if (ev.type == KeyPress) { + KeySym sym = XLookupKeysym(&ev.xkey, 0); + unsigned int kc = ev.xkey.keycode; + bool isLeftSoft = (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || kc == 135); + bool isRightSoft = (sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || kc == 166); + + if (isLeftSoft || isRightSoft) { + if (!initialSoftkeysReleased || nowMs() < startupIgnoreUntilMs) { + continue; + } + int which = (isLeftSoft ? 1 : 0) | (isRightSoft ? 2 : 0); + softkeyPending |= which | querySoftkeysDown(dpy); + if (softkeyPending == 3) { + writeSoftkeySuppress(nowMs() + 500); + hideWindow(false); + if (!sendPluginCommand("xdrawer", "TOGGLE")) { + runAsyncArgv("xdrawer"); + } + break; + } + if (softkeyDeadlineMs == 0) { + softkeyDeadlineMs = nowMs() + 40; + } + continue; + } + + // Physical Red End / Power button or Escape + if (sym == XK_Escape || sym == XF86XK_PowerOff || kc == 124) { + writeSoftkeySuppress(nowMs() + 350); + if (callState == CallState::Incoming || callState == CallState::Outgoing || + callState == CallState::Active) { + hangupCall(); + } else { + callState = CallState::Idle; + writeActiveStateFiles(); + hideWindow(true); + } + continue; + } + + // Physical Green Call button + if (sym == XF86XK_Phone || sym == XF86XK_PickupPhone || kc == 177) { + if (nowMs() < startupIgnoreUntilMs) { + continue; + } + if (callState == CallState::Incoming) { + acceptIncomingCall(); + } else if (callState == CallState::Ended) { + callState = CallState::Idle; + writeActiveStateFiles(); + hideWindow(true); + } else if (callState == CallState::Idle) { + writeSoftkeySuppress(nowMs() + 250); + hideWindow(false); + if (!sendPluginCommand("dialer", "OPEN")) { + runAsyncArgv("xdialer"); + } + } + continue; + } + + // Keypad digits 0-9 / * / # + char dialCh = mapDialKey(sym, kc, ev.xkey.state); + if (dialCh != '\0') { + if (callState == CallState::Incoming) { + if (dialCh == '*' || dialCh == '0') { + silenceRing(); + } else if (dialCh == '1') { + acceptIncomingCall(); + } else if (dialCh == '#') { + hangupCall(); + } + continue; + } + if (callState == CallState::Ended) { + callState = CallState::Idle; + writeActiveStateFiles(); + hideWindow(true); + continue; + } + if (dtmfMode) { + sendDtmfTone(dialCh); + continue; + } + if (dialCh == '1' || dialCh == '*') { + setMicMuted(!micMuted); + selectedRow = 0; + render(); + } else if (dialCh == '2' || dialCh == '#') { + toggleRecording(); + selectedRow = 1; + render(); + } else if (dialCh == '3') { + setSpeakerRoute(!speakerOn); + selectedRow = 2; + render(); + } else if (dialCh == '0') { + dtmfMode = !dtmfMode; + render(); + } else { + sendDtmfTone(dialCh); + } + continue; + } + + switch (sym) { + case XK_Up: + case XK_KP_Up: { + int maxRows = (callState == CallState::Incoming) ? 3 : 4; + selectedRow = (selectedRow + maxRows - 1) % maxRows; + render(); + break; + } + case XK_Down: + case XK_KP_Down: { + int maxRows = (callState == CallState::Incoming) ? 3 : 4; + selectedRow = (selectedRow + 1) % maxRows; + render(); + break; + } + case XK_Left: + case XK_KP_Left: + case XF86XK_AudioLowerVolume: + if (callState != CallState::Incoming) { + adjustVolume(-5); + selectedRow = 3; + render(); + } + break; + case XK_Right: + case XK_KP_Right: + case XF86XK_AudioRaiseVolume: + if (callState != CallState::Incoming) { + adjustVolume(+5); + selectedRow = 3; + render(); + } + break; + case XK_Return: + case XK_KP_Enter: + if (nowMs() < startupIgnoreUntilMs) { + break; + } + if (callState == CallState::Incoming) { + if (selectedRow == 0) { + acceptIncomingCall(); + } else if (selectedRow == 1) { + silenceRing(); + } else { + hangupCall(); + } + } else if (callState == CallState::Ended) { + callState = CallState::Idle; + writeActiveStateFiles(); + hideWindow(true); + } else { + if (selectedRow == 0) { + setMicMuted(!micMuted); + } else if (selectedRow == 1) { + toggleRecording(); + } else if (selectedRow == 2) { + setSpeakerRoute(!speakerOn); + } else if (selectedRow == 3) { + adjustVolume(callVolumePct >= 95 ? -50 : +10); + } + render(); + } + break; + default: + break; + } + } + } + + if (visible && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) { + int comb = softkeyPending | querySoftkeysDown(dpy); + softkeyPending = 0; + softkeyDeadlineMs = 0; + if (comb == 3) { + writeSoftkeySuppress(nowMs() + 500); + hideWindow(false); + if (!sendPluginCommand("xdrawer", "TOGGLE")) { + runAsyncArgv("xdrawer"); + } + } else if (comb == 1) { + if (callState == CallState::Incoming) { + acceptIncomingCall(); + } else if (callState == CallState::Outgoing || callState == CallState::Active) { + dtmfMode = !dtmfMode; + render(); + } else if (callState == CallState::Ended) { + callState = CallState::Idle; + writeActiveStateFiles(); + hideWindow(false); + if (!sendPluginCommand("calllog", "OPEN")) { + runAsyncArgv("xcalllog"); + } + } else { + hideWindow(false); + if (!sendPluginCommand("dialer", "OPEN")) { + runAsyncArgv("xdialer"); + } + } + } else if (comb == 2) { + if (callState == CallState::Incoming || callState == CallState::Outgoing || + callState == CallState::Active) { + hangupCall(); + } else { + callState = CallState::Idle; + writeActiveStateFiles(); + writeSoftkeySuppress(nowMs() + 350); + hideWindow(true); + } + } + } + + if (visible && (callState != CallState::Idle || checkRecordingAlive())) { + render(); + } + + int timeoutMs = -1; + if (visible || callState != CallState::Idle || checkRecordingAlive()) { + timeoutMs = 250; + if (softkeyDeadlineMs > 0) { + timeoutMs = static_cast(std::max(5, softkeyDeadlineMs - nowMs())); + } + } + + struct pollfd pfds[2] {}; + pfds[0].fd = xfd; + pfds[0].events = POLLIN; + pfds[1].fd = fifoFd; + pfds[1].events = POLLIN; + ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs); + } + + stopRecording(); + if (micMuted) { + setMicMuted(false); + } + ::unlink("/tmp/sxmo_call_ringing"); + ::unlink("/tmp/sxmo_active_call"); + hideWindow(false); + if (fifoFd >= 0) { + ::close(fifoFd); + } + ::unlink(getPluginFifoPath("callscreen").c_str()); + ::unlink(daemonPidPath.c_str()); + + g_object_unref(layoutIcon); + g_object_unref(layoutSmall); + g_object_unref(layoutMain); + g_object_unref(layoutNum); + g_object_unref(pangoCtx); + XftDrawDestroy(xftDraw); + XFreeGC(dpy, gc); + XFreePixmap(dpy, pix); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} + +int callscreen_plugin_init(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("callscreen", "--preload"); + } + return 0; +} + +int callscreen_on_global_key(const DwmHostApi* host, KeySym keysym, + unsigned int keycode, unsigned int state, + int is_press) { + (void)host; + if (!is_press || state != 0) { + return 0; + } + if (!isCallActiveOrRingingFast()) { + return 0; + } + // Green Call button: Accept ringing call or reopen active Call Screen + if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { + if (isCallRingingFast()) { + if (!sendPluginCommand("callscreen", "ACCEPT")) { + runAsyncArgv("xcallscreen", "accept"); + } + } else { + if (!sendPluginCommand("callscreen", "OPEN")) { + runAsyncArgv("xcallscreen"); + } + } + return 1; + } + // Red End button: Reject ringing call or hang up active call + if (keysym == XF86XK_PowerOff || keycode == 124) { + if (!sendPluginCommand("callscreen", "HANGUP")) { + runAsyncArgv("xcallscreen", "hangup"); + } + return 1; + } + return 0; +} + +int callscreen_on_idle_key(const DwmHostApi* host, KeySym keysym, + unsigned int keycode, unsigned int state) { + if (!host || state != 0) { + return 0; + } + if (!isCallActiveOrRingingFast()) { + return 0; + } + if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) { + if (isCallRingingFast()) { + sendPluginCommand("callscreen", "ACCEPT"); + } else { + sendPluginCommand("callscreen", "OPEN"); + } + return 1; + } + if (keysym == XF86XK_PowerOff || keycode == 124) { + sendPluginCommand("callscreen", "HANGUP"); + return 1; + } + return 0; +} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "callscreen", + "Incoming & active call screen with mic mute, speaker routing, and call recording", + callscreen_plugin_init, + nullptr, + nullptr, + callscreen_on_idle_key, + callscreen_on_global_key, + callscreen_run_main +}; diff --git a/src/dwm-plugins/dwm_plugin_runner.c b/src/dwm-plugins/dwm_plugin_runner.c index 157000a..d4b616e 100644 --- a/src/dwm-plugins/dwm_plugin_runner.c +++ b/src/dwm-plugins/dwm_plugin_runner.c @@ -160,6 +160,39 @@ static int try_send_plugin_fifo(const char *plugin_name, int sub_argc, char **su } else if (strcmp(plugin_name, "dmenu") == 0 && sub_argc >= 2 && sub_argv[1] && strcmp(sub_argv[1], "close") == 0) { len = snprintf(cmd, sizeof(cmd), "CLOSE\n"); + } else if (strcmp(plugin_name, "callscreen") == 0) { + if (sub_argc >= 2 && sub_argv[1] && sub_argv[1][0]) { + const char *verb = sub_argv[1]; + const char *a1 = (sub_argc >= 3 && sub_argv[2]) ? sub_argv[2] : ""; + const char *a2 = (sub_argc >= 4 && sub_argv[3]) ? sub_argv[3] : ""; + if (strcasecmp(verb, "incoming") == 0) { + len = snprintf(cmd, sizeof(cmd), "INCOMING %s %s\n", a1[0] ? a1 : "-", a2); + } else if (strcasecmp(verb, "outgoing") == 0) { + len = snprintf(cmd, sizeof(cmd), "OUTGOING %s %s\n", a1[0] ? a1 : "-", a2); + } else if (strcasecmp(verb, "active") == 0) { + len = snprintf(cmd, sizeof(cmd), "ACTIVE %s %s\n", a1[0] ? a1 : "-", a2); + } else if (strcasecmp(verb, "accept") == 0 || strcasecmp(verb, "pickup") == 0) { + len = snprintf(cmd, sizeof(cmd), "ACCEPT\n"); + } else if (strcasecmp(verb, "hangup") == 0 || strcasecmp(verb, "reject") == 0) { + len = snprintf(cmd, sizeof(cmd), "HANGUP\n"); + } else if (strcasecmp(verb, "mute") == 0 || strcasecmp(verb, "mute_ring") == 0 || + strcasecmp(verb, "ignore") == 0) { + len = snprintf(cmd, sizeof(cmd), "MUTE_RING\n"); + } else if (strcasecmp(verb, "ended") == 0 || strcasecmp(verb, "end") == 0) { + len = a1[0] ? snprintf(cmd, sizeof(cmd), "ENDED %s\n", a1) + : snprintf(cmd, sizeof(cmd), "ENDED\n"); + } else if (strcasecmp(verb, "close") == 0) { + len = snprintf(cmd, sizeof(cmd), "CLOSE\n"); + } else if (strcasecmp(verb, "record") == 0 || strcasecmp(verb, "toggle_rec") == 0) { + len = snprintf(cmd, sizeof(cmd), "TOGGLE_REC\n"); + } else if (strcasecmp(verb, "mic") == 0 || strcasecmp(verb, "toggle_mic") == 0) { + len = snprintf(cmd, sizeof(cmd), "TOGGLE_MIC\n"); + } else { + len = snprintf(cmd, sizeof(cmd), "OPEN %s\n", verb); + } + } else { + len = snprintf(cmd, sizeof(cmd), "OPEN\n"); + } } else if (sub_argc >= 2 && sub_argv[1] && strcmp(sub_argv[1], "newcontact") == 0) { const char *num = (sub_argc >= 3 && sub_argv[2]) ? sub_argv[2] : ""; len = snprintf(cmd, sizeof(cmd), "OPEN new:%s\n", num); @@ -203,6 +236,9 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) { if (strcasecmp(base, "xcalllog") == 0 || strcasecmp(base, "calllog") == 0) { return "calllog"; } + if (strcasecmp(base, "xcallscreen") == 0 || strcasecmp(base, "callscreen") == 0) { + return "callscreen"; + } if (strcasecmp(base, "xcontacts") == 0 || strcasecmp(base, "contacts") == 0) { return "contacts"; } diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index 0894122..de43f09 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -72,6 +72,12 @@ constexpr const char* kIconRefresh = "\xef\x80\xa1"; // U+F021 refresh constexpr const char* kIconSort = "\xef\x83\x9c"; // U+F0DC sort constexpr const char* kIconEye = "\xef\x81\xae"; // U+F06E eye/view constexpr const char* kIconPalette = "\xef\x87\xbc"; // U+F1FC paint-brush / theme +constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker +constexpr const char* kIconEarpiece = "\xef\x8a\xa0"; // U+F2A0 earpiece +constexpr const char* kIconSoundOff = "\xef\x91\xa6"; // U+F466 mute +constexpr const char* kIconMicOn = "\xf3\xb0\x8d\xac"; // U+F036C mic on +constexpr const char* kIconMicOff = "\xf3\xb0\x8d\xad"; // U+F036D mic off +constexpr const char* kIconRecord = "\xef\x84\x91"; // U+F111 circle / record dot inline int64_t nowMs() { struct timespec ts {}; @@ -211,7 +217,37 @@ inline bool isCallRingingFast() { } std::string ringPidFile = getRuntimeFile("sxmo.ring.pid"); int pid = readIntFile(ringPidFile, -1); - return (pid > 0 && ::kill(pid, 0) == 0); + if (pid > 0 && ::kill(pid, 0) == 0) { + return true; + } + if (::access("/tmp/sxmo_call_ringing", F_OK) == 0) { + return true; + } + return false; +} + +inline bool isCallActiveOrRingingFast() { + if (isCallRingingFast()) { + return true; + } + if (::access("/tmp/sxmo_active_call", F_OK) == 0) { + return true; + } + std::string callsDir = getRuntimeFile("sxmo_calls"); + DIR* d = ::opendir(callsDir.c_str()); + if (!d) { + return false; + } + bool found = false; + struct dirent* ent; + while ((ent = ::readdir(d)) != nullptr) { + if (ent->d_name[0] != '.' && ent->d_name[0] != '\0') { + found = true; + break; + } + } + ::closedir(d); + return found; } struct FileStamp { @@ -299,7 +335,8 @@ inline void runAsyncCmd(const std::string& cmd) { inline void runAsyncArgv(const char* prog, const char* arg1 = nullptr, - const char* arg2 = nullptr) { + const char* arg2 = nullptr, + const char* arg3 = nullptr) { pid_t pid = ::fork(); if (pid == 0) { ::setsid(); @@ -312,7 +349,13 @@ inline void runAsyncArgv(const char* prog, ::close(nullfd); } } - if (arg1 && arg2) { + if (arg1 && arg2 && arg3) { + execlp(prog, prog, arg1, arg2, arg3, static_cast(nullptr)); + std::string localBin = std::string("/home/user/.local/bin/") + prog; + execl(localBin.c_str(), prog, arg1, arg2, arg3, static_cast(nullptr)); + std::string usrBin = std::string("/usr/bin/") + prog; + execl(usrBin.c_str(), prog, arg1, arg2, arg3, static_cast(nullptr)); + } else if (arg1 && arg2) { execlp(prog, prog, arg1, arg2, static_cast(nullptr)); std::string localBin = std::string("/home/user/.local/bin/") + prog; execl(localBin.c_str(), prog, arg1, arg2, static_cast(nullptr)); @@ -949,6 +992,9 @@ inline void placePhoneCall(const std::string& rawNumber) { if (num.empty()) { return; } + if (!sendPluginCommand("callscreen", "OUTGOING - " + num)) { + runAsyncArgv("xcallscreen", "outgoing", "-", num.c_str()); + } runAsyncArgv("sxmo_modemdial.sh", num.c_str()); } @@ -961,6 +1007,7 @@ inline void closeOtherPlugins(const char* selfPluginName) { {"xdrawer.pid", "xdrawer"}, {"xdialer.pid", "dialer"}, {"xcalllog.pid", "calllog"}, + {"xcallscreen.pid", "callscreen"}, {"xcontacts.pid", "contacts"}, {"xsms.pid", "sms"}, {"xfiles.pid", "files"}, @@ -986,6 +1033,7 @@ inline void broadcastThemeReload(const char* selfPluginName = nullptr) { "xdrawer", "dialer", "calllog", + "callscreen", "contacts", "sms", "files", diff --git a/src/sxmo-dmenu/dmenu.c b/src/sxmo-dmenu/dmenu.c index 7af4697..6c2852e 100644 --- a/src/sxmo-dmenu/dmenu.c +++ b/src/sxmo-dmenu/dmenu.c @@ -1596,15 +1596,14 @@ append_active_call_entries(void) *sp = '\0'; char *slash = strrchr(line, '/'); snprintf(callid, sizeof(callid), "%s", slash ? (slash + 1) : line); - char label[256], act[256]; + char label[256]; snprintf(label, sizeof(label), "%s Incoming call %s", ICON_PHN, callid); - snprintf(act, sizeof(act), "sxmo_jobs.sh start incall_menu sxmo_modemcall.sh incoming_call_menu %s", callid); - add_menu_item(label, act, 0); + add_menu_item(label, "@plugin:callscreen", 0); } else if (!shown_incall) { shown_incall = 1; char label[128]; - snprintf(label, sizeof(label), "%s Incall Menu", ICON_PHN); - add_menu_item(label, "sxmo_jobs.sh start incall_menu sxmo_modemcall.sh incall_menu", 0); + snprintf(label, sizeof(label), "%s Call Screen", ICON_PHN); + add_menu_item(label, "@plugin:callscreen", 0); } } pclose(fp); @@ -1952,6 +1951,7 @@ close_other_active_plugins(void) { "xdrawer.pid", "/tmp/dwm_plugin_xdrawer.fifo" }, { "xdialer.pid", "/tmp/dwm_plugin_dialer.fifo" }, { "xcalllog.pid", "/tmp/dwm_plugin_calllog.fifo" }, + { "xcallscreen.pid", "/tmp/dwm_plugin_callscreen.fifo" }, { "xcontacts.pid", "/tmp/dwm_plugin_contacts.fifo" }, { "xsms.pid", "/tmp/dwm_plugin_sms.fifo" }, { "xfiles.pid", "/tmp/dwm_plugin_files.fifo" },