diff --git a/AGENTS.md b/AGENTS.md index 51bde70..8da8f30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ It uses a pure **X11 (`Xlibre` + `sxmo-de-dwm`)** stack (no Wayland) optimized f - `src/dwm-plugins/`: Preloaded C++20 `240x320` DWM plugins and shared headers: - `theme.h`: Unified `.Xresources` / `~/.xthemes/*.xth` parser, proportional font sizing (`theme.baseFontSize`), automatic shade derivation, and live Xft/Pango theme reloading (`PluginTheme`). - `phone_data.h`: Shared X11 geometry (`240x296` below the 24px bar), FIFO IPC (`/tmp/dwm_plugin_.fifo`), softkey chord detection, Sxmo TSV contact/call/SMS parsers, and `libjpeg`/`libpng` helpers. - - `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `callscreen_plugin.cpp` (`callscreen.so` / `xcallscreen`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`), `modem_plugin.cpp` (`modem.so` / `xmodem`), `networks_plugin.cpp` (`networks.so` / `xnetworks` / `xbluetooth`), `ringtones_plugin.cpp` (`ringtones.so` / `xringtones`), `wallpapers_plugin.cpp` (`wallpapers.so` / `xwallpapers` / `xbackground`). - - `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcallscreen`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `xmodem`, `xnetworks`, `xbluetooth`, `xringtones`, `xwallpapers`, `xbackground`, `dmenu`, and `xdmenu`. + - `dialer_plugin.cpp` (`dialer.so` / `xdialer`), `calllog_plugin.cpp` (`calllog.so` / `xcalllog`), `callscreen_plugin.cpp` (`callscreen.so` / `xcallscreen`), `contacts_plugin.cpp` (`contacts.so` / `xcontacts`), `sms_plugin.cpp` (`sms.so` / `xsms`), `files_plugin.cpp` (`files.so` / `xfiles`), `camera_plugin.cpp` (`camera.so` / `xcamera`), `themes_plugin.cpp` (`themes.so` / `xthemes`), `modem_plugin.cpp` (`modem.so` / `xmodem`), `networks_plugin.cpp` (`networks.so` / `xnetworks` / `xbluetooth`), `ringtones_plugin.cpp` (`ringtones.so` / `xringtones`), `wallpapers_plugin.cpp` (`wallpapers.so` / `xwallpapers` / `xbackground`), `music_plugin.cpp` (`music.so` / `xmusic` / `xplayer`). + - `dwm_plugin_runner.c`: Lightweight `/usr/bin/dwm-plugin` launcher symlinked by `xdrawer`, `xdialer`, `xcalllog`, `xcallscreen`, `xcontacts`, `xsms`, `xfiles`, `xcamera`, `xthemes`, `xmodem`, `xnetworks`, `xbluetooth`, `xringtones`, `xwallpapers`, `xbackground`, `xmusic`, `xplayer`, `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`). diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f0157..e3271b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,12 +25,12 @@ Initial public release of **Bananarchy Linux**, focused on complete hardware and ### Preloaded native keypad applications - **System and context menus (`dmenu`)**: resident menu daemon with instant (`< 2 ms`) wakeup, 10-line vertical layout, live T9 filtering, and built-in submenus and hardware toggles. -- **Global controls drawer (`xdrawer`)**: instant overlay toggled by pressing both softkeys together to adjust speaker volume, earpiece volume, ringer mode, flashlight, sound/microphone mute, screen brightness, and keypad backlight. +- **Global controls drawer (`xdrawer`)**: instant overlay toggled by pressing both softkeys together to adjust speaker volume, earpiece volume, ringtone volume, ringer mode, flashlight, sound/microphone mute, screen brightness, and keypad backlight. - **Phone dialer (`xdialer`), call log (`xcalllog`), and call screen (`xcallscreen`)**: home-screen digit dialing with live T9 contact matching, interactive carrier USSD sessions (`*100#`, etc.), Dual-SIM `*#06#` IMEI inspection, call history filtering, and an in-call screen with speaker/earpiece switching, microphone mute, DTMF tones, and one-key WAV call recording (`~/Recordings/`). - **Contacts (`xcontacts`) and SMS manager (`xsms`)**: full phonebook editor with direct Qualcomm QMI UIM SIM card contact import and export across SIM 1 and SIM 2 (`EF_ADN`), paired with a threaded SMS conversation view and multi-tap message composer with draft support. - **Modem and Dual-SIM manager (`xmodem`)**: 4-tab cellular control center (`SIMs`, `Radio`, `AT`, `Log`) supporting physical SIM 1 / SIM 2 slot switching, SIM PIN unlock and PUK unblock wizards, PIN enable/disable/change, 2G/3G/4G radio mode selection, USSD requests, and a live AT command console. - **Networks and Bluetooth manager (`xnetworks`)**: 3-tab connectivity manager for Wi-Fi scanning and WPA2 entry, Wi-Fi hotspot, cellular data and APN configuration, USB tethering, and Bluetooth pairing. -- **File manager (`xfiles`) and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers, plus a live V4L2 camera viewfinder with digital zoom (`1x`/`2x`/`4x`), auto-flash, continuous torch, 3-second self-timer, and photo gallery. +- **File manager (`xfiles`), music and tracker player (`xmusic`), and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers (including one-click actions to preview images, set any image as wallpaper, play audio, or install any audio file as ringtone), a 3-tab music and chiptune tracker module player (`WAV`, `MP3`, `OGG`, `FLAC`, `Opus`, `MOD`, `XM`, `IT`, `S3M`, and more), and a live V4L2 camera viewfinder with digital zoom (`1x`/`2x`/`4x`), auto-flash, continuous torch, 3-second self-timer, and photo gallery. - **Themes (`xthemes`), wallpapers (`xwallpapers`), and ringtones (`xringtones`)**: unified `.Xresources` theming engine (`~/.xthemes/*.xth`) with 20 preinstalled color schemes, live zero-restart reload across all apps and terminals, interactive color/font customization, wallpaper scaling and preview, and ringtone/vibration settings. ### Keypad input, terminal, games, and developer tools diff --git a/MANUAL.md b/MANUAL.md index ab55f64..9244d92 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -25,7 +25,7 @@ This manual covers flashing, operating, and customizing **Bananarchy Linux** (`2 - Uses a lightweight cross-compiled **Xlibre** server with `modesetting` KMS, partial-rectangle `ShadowFB` SPI updates, and zero Wayland overhead. - **Patched `dwm`** provides a `240x320` vertical bottom-stack (`bstack`) layout, a 24px top status bar, a transparent `"Menu"` / `"Names"` desktop softkey bar, a Conky desktop clock and status widget (which refreshes automatically when changing time, timezone, or wallpaper, or when logging out), and `< 2 ms` handling of physical slider (`SW_LID`) and wake keys. 2. **Preloaded resident DWM plugins (`< 2 ms` wakeup)**: - - Core phone applications (`dmenu`, `xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`, `modem`, `networks`, `ringtones`, `wallpapers`) run as preloaded resident daemons (`dwm-plugin --preload`) that sleep at 0% CPU and open instantaneously over FIFO/socket IPC. + - Core phone applications (`dmenu`, `xdrawer`, `dialer`, `calllog`, `callscreen`, `contacts`, `sms`, `files`, `camera`, `themes`, `modem`, `networks`, `ringtones`, `wallpapers`, `music`) run as preloaded resident daemons (`dwm-plugin --preload`) that sleep at 0% CPU and open instantaneously over FIFO/socket IPC. - All UI elements share a single **XRDB theming engine** (`~/.xthemes/*.xth` -> `~/.Xresources`) with 20 preinstalled color schemes, proportional font sizing (`theme.baseFontSize`), and live reload without restarting X11. 3. **First-class terminal (`st`) and 6-mode `fcitx5-t9` keypad input**: - `st` is patched with HarfBuzz OpenType ligature shaping (`Fira Code` default) and live `.Xresources` reload. @@ -120,7 +120,7 @@ You can switch layouts and focus using either physical keypad multi-presses or a - `Mod4+Shift+q`: close the focused window. #### 3.1.2 Preloaded native DWM applications -All fourteen phone applications are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup, and awakened over `/tmp/dwm_plugin_.fifo` in `< 2 ms`: +All fifteen phone applications are compiled as shared-object plugins (`/usr/lib/dwm/plugins/*.so`), preloaded at `dwm` startup, and awakened over `/tmp/dwm_plugin_.fifo` in `< 2 ms`: 1. **System and context menus (`dmenu.so` / `dmenu` / `xdmenu`)**: - **Open / close**: press **Left softkey (`Menu`)** for the **System menu** or **Right softkey (`Back`)** inside an application for its **Context menu**; press the same softkey or `Escape` to close. @@ -129,7 +129,7 @@ All fourteen phone applications are compiled as shared-object plugins (`/usr/lib 2. **Global phone controls (`xdrawer.so` / `xdrawer`)**: - **Open / close**: press **both softkeys simultaneously** (`KEY_MENU` + `KEY_BACK`) from anywhere, or select **XDrawer** from the System menu. - - **Controls**: use `Up`/`Down` (or `1`–`8`) to select **Speaker Volume**, **Earpiece Volume**, **Ringer Mode** (`Ring+Vib` / `Vibrate` / `Silent` / `DND`), **Flashlight**, **Sound**, **Microphone**, **Screen Brightness**, or **Keypad Backlight**; use `Left`/`Right` or `Return` to adjust or toggle. + - **Controls**: use `Up`/`Down` (or `1`–`9`) to select **Speaker**, **Earpiece**, **Ring Vol**, **Ringer** (`Ring+Vib` / `Vibrate` / `Silent` / `DND`), **Flashlight**, **Sound**, **Microphone**, **Screen**, or **Keypad**; use `Left`/`Right` or `Return` to adjust or toggle. 3. **Phone dialer (`dialer.so` / `xdialer`)**: - **Open**: press any **`0`–`9`, `*`, or `#` key** on the empty desktop, or open **Dialer** from the System menu or Call Log. @@ -166,7 +166,7 @@ All fourteen phone applications are compiled as shared-object plugins (`/usr/lib 8. **File manager (`files.so` / `xfiles`)**: - **Open**: select **Files** from the System menu or press `Mod4+d`. - - **Controls and viewers**: navigate directories with `Up`/`Down`/`Left`/`Right`/`Return`, filter with `0`–`9`, toggle dotfiles with `*`, and cycle sort order (**Name** / **Date** / **Size**) with `#`. Includes built-in text and image (`libjpeg`/`libpng`) viewers plus file operations (**Edit in Nano**, **New Folder**, **New File**, **Rename**, **Copy**, **Cut / Move**, **Paste Here**, **Properties**, **Open Terminal Here**, **Delete**) on the **Left softkey**. + - **Controls and viewers**: navigate directories with `Up`/`Down`/`Left`/`Right`/`Return`, filter with `0`–`9`, toggle dotfiles with `*`, and cycle sort order (**Name** / **Date** / **Size**) with `#`. Pressing `Return` on an image opens the built-in image viewer (`*` toggles full-screen preview, **Left softkey (`Set Wall`)** installs and applies the image as the desktop wallpaper); pressing `Return` on an audio or tracker file opens it in **Music (`xmusic`)**. The **Left softkey (`Actions`)** menu provides **Preview Image**, **Set as Wallpaper**, **Play in Music Player**, **Set as Ringtone**, **View Text**, **Edit in Nano**, **New Folder**, **New File**, **Rename**, **Copy**, **Cut / Move**, **Paste Here**, **Properties**, **Open Terminal Here**, and **Delete**. 9. **Camera (`camera.so` / `xcamera`)**: - **Open**: select **Camera** from the System menu or run `xcamera`. @@ -193,12 +193,20 @@ All fourteen phone applications are compiled as shared-object plugins (`/usr/lib 13. **Ringtones and ringer manager (`ringtones.so` / `xringtones`)**: - **Open**: select **Ringtones** from the System, Config, or Audio menu, or run `xringtones`. - - **Controls**: preview built-in and user ringtones (`~/Ringtones/`, `~/Music/`) with `*` or **Left softkey (`Play` / `Stop`)**, press `Return` to set the active ringtone, and switch to **`2:Settings`** to configure **Ringer Mode**, **Ring Duration**, **Ring Loop Count**, and **Vibrate Strength**. + - **Controls**: preview built-in and user ringtones (`~/Ringtones/`, `~/Music/`) with `*` or **Left softkey (`Play` / `Stop`)**, press `Return` to set the active ringtone, and switch to **`2:Settings`** to configure **Ringer Mode**, **Ring Volume**, **Ring Duration**, **Ring Loop Count**, and **Vibrate Strength**. 14. **Desktop wallpaper manager (`wallpapers.so` / `xwallpapers` / `xbackground`)**: - **Open**: select **Wallpapers** from the System, Config, or Apps menu, or run `xwallpapers`. - **Controls**: browse system and user wallpapers with a live portrait thumbnail, cycle scaling mode (`Fill`, `Scale`, `Fit`, `Center`, `Tile`) with `Left`/`Right` or `#`, toggle full-screen preview with `*`, and press `Return` to apply the wallpaper and immediately refresh the Conky desktop overlay. +15. **Music and tracker module player (`music.so` / `xmusic` / `xplayer`)**: + - **Open**: select **Music** from the System, Applications, or Audio menu, open any audio/tracker file from **Files (`xfiles`)**, or run `xmusic [file_or_dir]`. + - **Supported formats**: plays standard audio (`.wav`, `.mp3`, `.ogg`, `.flac`, `.opus`, `.m4a`, `.aac`) as well as classic chiptune/tracker modules (`.mod`, `.xm`, `.it`, `.s3m`, `.mptm`, `.stm`, `.med`, `.okt`, `.669`, `.ult`, `.far`, `.mtm`, `.ptm`, `.amf`, `.dsm`, `.imf`, `.gdm`, `.stx`, `.umx`, `.mo3`) via an embedded `mpv` + `FFmpeg` + `libopenmpt` backend that automatically pauses on incoming calls. + - **Three tabs (`1:Player`, `2:Queue`, `3:Files` — switch with `1`–`3` or `Left`/`Right` in Queue/Files)**: + - **`1:Player`**: displays format badge (`MOD`, `XM`, `MP3`, etc.), scrolling track title, artist/metadata, animated equalizer bars, elapsed/total progress bar, repeat mode (`Off`, `All`, `One`), shuffle state, and volume. Press `Return` or `5` to **Play/Pause**, `Left`/`Right` (`4`/`6`) to **Seek `-5s`/`+5s`**, `Up`/`Down` (`2`/`8`) to adjust **Volume**, `7`/`9` for **Prev/Next track**, `*` to cycle **Repeat**, `#` to toggle **Shuffle**, `0` to **Stop**, or **Left softkey (`Options`)** to install the current track as the phone ringtone. + - **`2:Queue`**: browse the active playlist queue, filter with `0`–`9`, press `Return` to jump to a track, `*` to remove a track, `#` to shuffle, or **Left softkey (`Actions`)** to set the selected track as ringtone or clear the queue. + - **`3:Files`**: browse the filesystem (`~/Music`, `/usr/share/sxmo/music`, `/usr/share/sxmo/ringtones`, or any folder), press `Return` to play a file and load its folder into the queue, `*` to append a file or folder to the queue, or `#` to play the entire current folder. + #### 3.1.3 Standalone keypad games (`Snake` and `Tetris`) Two classic X11 games (`/usr/bin/snake` and `/usr/bin/tetris`) are accessible from **System menu -> Games**: - **Snake (`snake`)**: steer with `2`/`8`/`4`/`6` or D-Pad; pause or restart with `5`/`Return`/`#`/`*`; quit with `Right softkey`/`BackSpace`/`Escape`. diff --git a/README.md b/README.md index 71f94cf..f71d04a 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ ### Keypad-first user experience and native DWM plugins Instead of spawning slow shell scripts for everyday phone tasks, Bananarchy runs a suite of preloaded C/C++ plugins inside DWM that sleep at 0% CPU and open in `< 2 ms`: - **System and context menus (`dmenu`)**: instant full-screen 10-line vertical menus with live T9 filtering, built-in submenus, and hardware toggles. -- **Global controls drawer (`xdrawer`)**: press **both softkeys together** from anywhere to adjust speaker and earpiece volume, ringer mode, flashlight, sound and microphone mute, screen brightness, and keypad backlight. +- **Global controls drawer (`xdrawer`)**: press **both softkeys together** from anywhere to adjust speaker, earpiece, and ringtone volume, ringer mode, flashlight, sound and microphone mute, screen brightness, and keypad backlight. - **Phone dialer (`xdialer`), call log (`xcalllog`), and call screen (`xcallscreen`)**: press any digit on the home screen to start dialing with live T9 contact matching, run interactive carrier USSD codes (`*100#`, etc.), or type `*#06#` to inspect both IMEIs. Incoming and active calls open a dedicated call screen with earpiece/speaker switching, microphone mute, DTMF keypad, and one-key call recording (`~/Recordings/`). - **Contacts (`xcontacts`) and SMS manager (`xsms`)**: full phonebook editor with direct Qualcomm QMI SIM card contact import and export across SIM 1 and SIM 2, paired with a threaded SMS conversation view and multi-tap message composer. - **Modem and Dual-SIM manager (`xmodem`)**: 4-tab cellular control center (`SIMs`, `Radio`, `AT`, `Log`) supporting physical SIM 1 / SIM 2 active slot switching, SIM PIN unlock and PUK unblock wizards, 2G/3G/4G radio mode selection, interactive USSD sessions, and a live AT command console. - **Networks and Bluetooth manager (`xnetworks`)**: 3-tab connectivity manager for Wi-Fi networks, WPA2 password entry, Wi-Fi hotspot, USB tethering, mobile data and APN editing, and Bluetooth pairing. -- **File manager (`xfiles`) and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers, plus a live V4L2 camera viewfinder with digital zoom, flash/torch control, self-timer, and photo gallery. +- **File manager (`xfiles`), music and tracker player (`xmusic`), and camera (`xcamera`)**: keypad directory browser with built-in text and image viewers (plus one-click actions to play audio, preview images, set any image as wallpaper, or install any audio file as ringtone), a 3-tab music and chiptune tracker module player (`WAV`, `MP3`, `OGG`, `FLAC`, `Opus`, `MOD`, `XM`, `IT`, `S3M`, and more), and a live V4L2 camera viewfinder with digital zoom, flash/torch control, self-timer, and photo gallery. - **Themes (`xthemes`), wallpapers (`xwallpapers`), and ringtones (`xringtones`)**: installable `.Xresources` color and font themes (`~/.xthemes/*.xth`, 20 preinstalled schemes) with live zero-restart reload across the entire UI and terminal, alongside dedicated managers for desktop wallpapers and call ringtones/vibration. ### Input, terminal, and developer tools diff --git a/overlay/rootfs/etc/os-release b/overlay/rootfs/etc/os-release index a3d04b8..93029a8 100644 --- a/overlay/rootfs/etc/os-release +++ b/overlay/rootfs/etc/os-release @@ -4,8 +4,8 @@ VERSION_ID="@BANANARCHY_VERSION@" VERSION="@BANANARCHY_VERSION@ (msm8909)" ID="bananarchy" ID_LIKE="postmarketos alpine" -HOME_URL="https://bananahackers.net/" -SUPPORT_URL="https://wiki.nura.eco/wiki/Nokia_8110_4G_(nokia-argon)" -BUG_REPORT_URL="https://gitlab.postmarketos.org/postmarketOS/pmaports/issues" +HOME_URL="https://bananarchy.top" +SUPPORT_URL="https://bananarchy.top" +BUG_REPORT_URL="https://bananarchy.top" LOGO="bananarchy" ANSI_COLOR="1;33" diff --git a/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_start.sh b/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_start.sh index 19b6471..1d0579c 100755 --- a/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_start.sh +++ b/overlay/rootfs/home/user/.config/sxmo/hooks/sxmo_hook_start.sh @@ -43,6 +43,24 @@ if [ -z "$SXMO_NO_AUDIO" ]; then # monitor for headphone for statusbar superctl start sxmo_soundmonitor + ( + sleep 1 + spk_vol=100 + if [ -r "$XDG_CONFIG_HOME/sxmo/speaker_vol" ]; then + read -r spk_vol < "$XDG_CONFIG_HOME/sxmo/speaker_vol" 2>/dev/null || spk_vol=100 + fi + case "$spk_vol" in ''|*[!0-9]*) spk_vol=100 ;; esac + if [ "$spk_vol" -le 0 ]; then + amixer -c 0 cset "name='RX3 Digital Volume'" 0 >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ 0% >/dev/null 2>&1 || true + else + rx3_hw=$(( 60 + spk_vol * 32 / 100 )) + amixer -c 0 cset "name='SPK DAC Switch'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 Digital Volume'" "$rx3_hw" >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ "${spk_vol}%" >/dev/null 2>&1 || true + fi + sxmo_hook_statusbar.sh volume >/dev/null 2>&1 || true + ) & fi # Periodically update some status bar components diff --git a/overlay/rootfs/home/user/Music/Bananarchy_Chip.mod b/overlay/rootfs/home/user/Music/Bananarchy_Chip.mod new file mode 100644 index 0000000..7c95059 Binary files /dev/null and b/overlay/rootfs/home/user/Music/Bananarchy_Chip.mod differ diff --git a/overlay/rootfs/lib/modules/6.12.1-msm8916/kernel/drivers/input/misc/pm8xxx-vibrator.ko.zst b/overlay/rootfs/lib/modules/6.12.1-msm8916/kernel/drivers/input/misc/pm8xxx-vibrator.ko.zst new file mode 100644 index 0000000..48d5094 Binary files /dev/null and b/overlay/rootfs/lib/modules/6.12.1-msm8916/kernel/drivers/input/misc/pm8xxx-vibrator.ko.zst differ diff --git a/overlay/rootfs/usr/bin/dwm-plugin b/overlay/rootfs/usr/bin/dwm-plugin index 997e3b7..ca67f48 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_vibrate b/overlay/rootfs/usr/bin/sxmo_vibrate new file mode 100755 index 0000000..20ce6dc Binary files /dev/null and b/overlay/rootfs/usr/bin/sxmo_vibrate differ diff --git a/overlay/rootfs/usr/bin/xmusic b/overlay/rootfs/usr/bin/xmusic new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xmusic @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/xplayer b/overlay/rootfs/usr/bin/xplayer new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/overlay/rootfs/usr/bin/xplayer @@ -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 d24c14e..025e74a 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/calllog.so and b/overlay/rootfs/usr/lib/dwm/plugins/calllog.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so index b0bbacf..be4416f 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so and b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/camera.so b/overlay/rootfs/usr/lib/dwm/plugins/camera.so index cd22e27..918bdc0 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 eb7833e..5acd39a 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 c788fde..bf15e44 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 0a3e077..75aaf70 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 f71abba..50e8813 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/files.so and b/overlay/rootfs/usr/lib/dwm/plugins/files.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/modem.so b/overlay/rootfs/usr/lib/dwm/plugins/modem.so index 2d1d849..f2b58f2 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/modem.so and b/overlay/rootfs/usr/lib/dwm/plugins/modem.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/music.so b/overlay/rootfs/usr/lib/dwm/plugins/music.so new file mode 100755 index 0000000..fcedc18 Binary files /dev/null and b/overlay/rootfs/usr/lib/dwm/plugins/music.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/networks.so b/overlay/rootfs/usr/lib/dwm/plugins/networks.so index 659cbdd..a0163b0 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/networks.so and b/overlay/rootfs/usr/lib/dwm/plugins/networks.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so b/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so index 99f492c..98c633b 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so and b/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/sms.so b/overlay/rootfs/usr/lib/dwm/plugins/sms.so index 1cb2376..32916dc 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 1cc8224..5783928 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/themes.so and b/overlay/rootfs/usr/lib/dwm/plugins/themes.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/wallpapers.so b/overlay/rootfs/usr/lib/dwm/plugins/wallpapers.so index 32186ae..2e8c19c 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/wallpapers.so and b/overlay/rootfs/usr/lib/dwm/plugins/wallpapers.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so index 9719707..9031a32 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so and b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/overlay/rootfs/usr/share/alsa/ucm2/codecs/msm8916-wcd/Speaker.conf b/overlay/rootfs/usr/share/alsa/ucm2/codecs/msm8916-wcd/Speaker.conf new file mode 100644 index 0000000..1ee2011 --- /dev/null +++ b/overlay/rootfs/usr/share/alsa/ucm2/codecs/msm8916-wcd/Speaker.conf @@ -0,0 +1,27 @@ +SectionDevice."Speaker" { + Comment "Speaker playback" + + ConflictingDevice [ + "Headphones" + "Earpiece" + ] + + EnableSequence [ + cset "name='SPK DAC Switch' 1" + cset "name='RX3 MIX1 INP1' RX1" + ## gain to +8dB + cset "name='RX3 Digital Volume' 92" + ] + + DisableSequence [ + cset "name='RX3 Digital Volume' 0" + cset "name='SPK DAC Switch' 0" + cset "name='RX3 MIX1 INP1' ZERO" + ] + + Value { + PlaybackPCM "${var:WcdPlaybackPCM}" + PlaybackChannels 2 + PlaybackPriority 200 + } +} diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_apps.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_apps.sh index 3c350e0..0f796ca 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_apps.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_apps.sh @@ -50,6 +50,7 @@ write_line_app baobab "$icon_pie Baobab" "baobab" write_line_app bluetuith "$icon_bth Bluetuith" "sxmo_terminal.sh bluetuith" write_line_app calcurse "$icon_clk Calcurse" "sxmo_terminal.sh calcurse" write_line_app xcamera "$icon_cam Camera" "xcamera" +write_line_app xmusic "$icon_mus Music" "xmusic" write_line_app celluloid "$icon_mvi Celluloid" "celluloid" write_line_app chatty "$icon_msg Chatty" "chatty" ([ -z "$WAYLAND_DISPLAY" ] && command -v clipmenu >/dev/null) && \ diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh index d346411..72bf0b6 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh @@ -27,8 +27,8 @@ fi # pm8xxx-vib shifts FF_RUMBLE strong_magnitude right by 8 bits (valid 256..65535) case "${SXMO_VIBRATE_STRENGTH:-}" in - 1) SXMO_VIBRATE_STRENGTH=20000 ;; - 2|"") SXMO_VIBRATE_STRENGTH=40000 ;; + 1) SXMO_VIBRATE_STRENGTH=8000 ;; + 2|"") SXMO_VIBRATE_STRENGTH=32000 ;; 3) SXMO_VIBRATE_STRENGTH=65535 ;; esac @@ -54,15 +54,41 @@ if ! sxmo_modemcall.sh list_active_calls \ exit fi +case "${SXMO_RINGVOL:-100}" in + ''|*[!0-9]*) SXMO_RINGVOL=100 ;; +esac +[ "$SXMO_RINGVOL" -gt 100 ] && SXMO_RINGVOL=100 + +SAVED_RX3="$(amixer -c 0 cget "name='RX3 Digital Volume'" 2>/dev/null | awk -F= '/: values=/ {print $2; exit}')" +SAVED_SPK_VOL=100 +if [ -r "$XDG_CONFIG_HOME/sxmo/speaker_vol" ]; then + read -r SAVED_SPK_VOL < "$XDG_CONFIG_HOME/sxmo/speaker_vol" 2>/dev/null || SAVED_SPK_VOL=100 +fi +case "$SAVED_SPK_VOL" in ''|*[!0-9]*) SAVED_SPK_VOL=100 ;; esac + finish() { trap - INT TERM EXIT - jobs -p | xargs -r kill - wait + jobs -p | xargs -r kill 2>/dev/null || true + wait 2>/dev/null || true + if [ -n "${SAVED_RX3:-}" ]; then + amixer -c 0 cset "name='RX3 Digital Volume'" "$SAVED_RX3" >/dev/null 2>&1 || true + fi + wpctl set-volume @DEFAULT_AUDIO_SINK@ "${SAVED_SPK_VOL}%" >/dev/null 2>&1 || true exit } ring() { + if [ "${SXMO_RINGVOL:-100}" -le 0 ]; then + return 0 + fi + ring_rx3=$(( 60 + SXMO_RINGVOL * 34 / 100 )) + mpv_vol=$(( SXMO_RINGVOL * 130 / 100 )) + amixer -c 0 cset "name='SPK DAC Switch'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 Digital Volume'" "$ring_rx3" >/dev/null 2>&1 || true + wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ 100% >/dev/null 2>&1 || true mpv --no-resume-playback --quiet --no-video \ + --volume-max=150 --volume="$mpv_vol" \ --loop="${SXMO_RINGNUMBER:-10}" "$SXMO_RINGTONE" >/dev/null & } @@ -90,6 +116,9 @@ if [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ] && [ ! -f "$XDG_CONFIG_HOME"/sxmo/.n sxmo_log "RING AND VIBRATE" sxmo_playerctl.sh pause_all + if [ -p /tmp/dwm_plugin_music.fifo ]; then + echo "PAUSE" > /tmp/dwm_plugin_music.fifo 2>/dev/null || true + fi ring vibrate @@ -99,6 +128,9 @@ elif [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ] && [ -f "$XDG_CONFIG_HOME"/sxmo/.n sxmo_log "RING ONLY" sxmo_playerctl.sh pause_all + if [ -p /tmp/dwm_plugin_music.fifo ]; then + echo "PAUSE" > /tmp/dwm_plugin_music.fifo 2>/dev/null || true + fi ring diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_sms.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_sms.sh index 9108556..99baa0d 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_sms.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_sms.sh @@ -30,12 +30,23 @@ if [ -f "$XDG_CONFIG_HOME"/sxmo/.busy ]; then fi fi +if [ -f "$XDG_CONFIG_HOME/sxmo/ringsettings" ]; then + # shellcheck disable=SC1091 + . "$XDG_CONFIG_HOME/sxmo/ringsettings" +fi + if [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ]; then - mpv --no-resume-playback --quiet --no-video "$SXMO_TEXTSOUND" >> /dev/null 2>&1 & + case "${SXMO_RINGVOL:-100}" in + ''|*[!0-9]*) SXMO_RINGVOL=100 ;; + esac + if [ "$SXMO_RINGVOL" -gt 0 ]; then + mpv_vol=$(( SXMO_RINGVOL * 130 / 100 )) + mpv --no-resume-playback --quiet --no-video --volume-max=150 --volume="$mpv_vol" "$SXMO_TEXTSOUND" >> /dev/null 2>&1 & + fi fi if [ ! -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ]; then - sxmo_vibrate 500 "${SXMO_VIBRATE_STRENGTH:-1}" & + sxmo_vibrate 500 "${SXMO_VIBRATE_STRENGTH:-2}" & fi wait diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_start.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_start.sh index 19b6471..1d0579c 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_start.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_start.sh @@ -43,6 +43,24 @@ if [ -z "$SXMO_NO_AUDIO" ]; then # monitor for headphone for statusbar superctl start sxmo_soundmonitor + ( + sleep 1 + spk_vol=100 + if [ -r "$XDG_CONFIG_HOME/sxmo/speaker_vol" ]; then + read -r spk_vol < "$XDG_CONFIG_HOME/sxmo/speaker_vol" 2>/dev/null || spk_vol=100 + fi + case "$spk_vol" in ''|*[!0-9]*) spk_vol=100 ;; esac + if [ "$spk_vol" -le 0 ]; then + amixer -c 0 cset "name='RX3 Digital Volume'" 0 >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ 0% >/dev/null 2>&1 || true + else + rx3_hw=$(( 60 + spk_vol * 32 / 100 )) + amixer -c 0 cset "name='SPK DAC Switch'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 Digital Volume'" "$rx3_hw" >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ "${spk_vol}%" >/dev/null 2>&1 || true + fi + sxmo_hook_statusbar.sh volume >/dev/null 2>&1 || true + ) & fi # Periodically update some status bar components diff --git a/overlay/rootfs/usr/share/sxmo/music/Bananarchy_Chip.mod b/overlay/rootfs/usr/share/sxmo/music/Bananarchy_Chip.mod new file mode 100644 index 0000000..7c95059 Binary files /dev/null and b/overlay/rootfs/usr/share/sxmo/music/Bananarchy_Chip.mod differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav index 9e17490..caaf356 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Banana_Chime.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav index cdbe3d2..9d34337 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Classic_Nokia.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav index b4907dc..0c9791b 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Digital_Pulse.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav index 4158aed..b147199 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Marimba_Short.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav index 3ad50f9..8115a17 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Retro_Beep.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav index da8b66e..d636488 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Soft_Bell.wav differ diff --git a/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav b/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav index 382d4e4..52b9f28 100644 Binary files a/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav and b/overlay/rootfs/usr/share/sxmo/ringtones/Urgent_Alert.wav differ diff --git a/prebuilt/kernel-6.12.1-msm8916/pm8xxx-vibrator.ko.zst b/prebuilt/kernel-6.12.1-msm8916/pm8xxx-vibrator.ko.zst new file mode 100644 index 0000000..48d5094 Binary files /dev/null and b/prebuilt/kernel-6.12.1-msm8916/pm8xxx-vibrator.ko.zst differ diff --git a/prebuilt/sxmo-x11/usr/bin/dwm-plugin b/prebuilt/sxmo-x11/usr/bin/dwm-plugin index 997e3b7..ca67f48 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/sxmo_vibrate b/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate new file mode 100755 index 0000000..20ce6dc Binary files /dev/null and b/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate differ diff --git a/prebuilt/sxmo-x11/usr/bin/xmusic b/prebuilt/sxmo-x11/usr/bin/xmusic new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xmusic @@ -0,0 +1 @@ +dwm-plugin \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/bin/xplayer b/prebuilt/sxmo-x11/usr/bin/xplayer new file mode 120000 index 0000000..e492465 --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/xplayer @@ -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 d24c14e..025e74a 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so index b0bbacf..be4416f 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/camera.so index cd22e27..918bdc0 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 eb7833e..5acd39a 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 c788fde..bf15e44 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 0a3e077..75aaf70 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 f71abba..50e8813 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/files.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so index 2d1d849..f2b58f2 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/music.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/music.so new file mode 100755 index 0000000..fcedc18 Binary files /dev/null and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/music.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so index 659cbdd..a0163b0 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so index 99f492c..98c633b 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/sms.so index 1cb2376..32916dc 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 1cc8224..5783928 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/themes.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so index 32186ae..2e8c19c 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so index 9719707..9031a32 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/scripts/build-sxmo-x11.sh b/scripts/build-sxmo-x11.sh index 94c4510..3018a9f 100755 --- a/scripts/build-sxmo-x11.sh +++ b/scripts/build-sxmo-x11.sh @@ -158,6 +158,10 @@ EOF /bananarchy/src/dwm-plugins/wallpapers_plugin.cpp $PLUG_LIBS $MEDIA_LIBS \ -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so + $CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \ + /bananarchy/src/dwm-plugins/music_plugin.cpp $PLUG_LIBS \ + -o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/music.so + $STRIP --strip-unneeded \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so \ @@ -171,13 +175,19 @@ EOF /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/modem.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so \ /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so \ - /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/wallpapers.so \ + /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/music.so $CC $OPT_CFLAGS -std=c99 -D_DEFAULT_SOURCE -Wall -Wextra \ /bananarchy/src/dwm-plugins/dwm_plugin_runner.c -ldl \ -o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm-plugin $STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm-plugin + $CC $OPT_CFLAGS -std=c99 -D_DEFAULT_SOURCE -Wall -Wextra \ + /bananarchy/src/dwm-plugins/sxmo_vibrate.c \ + -o /bananarchy/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate + $STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xdmenu ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer @@ -196,6 +206,8 @@ EOF ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xringtones ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xwallpapers ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xbackground + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xmusic + ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xplayer mkdir -p /bananarchy/overlay/rootfs/usr/bin /bananarchy/overlay/rootfs/usr/lib/dwm/plugins cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/ diff --git a/src/dwm-plugins/dwm_plugin_runner.c b/src/dwm-plugins/dwm_plugin_runner.c index e3cfe7f..239a36a 100644 --- a/src/dwm-plugins/dwm_plugin_runner.c +++ b/src/dwm-plugins/dwm_plugin_runner.c @@ -281,6 +281,10 @@ static const char *resolve_plugin_name_from_argv0(const char *argv0) { strcasecmp(base, "xbackground") == 0 || strcasecmp(base, "background") == 0) { return "wallpapers"; } + if (strcasecmp(base, "xmusic") == 0 || strcasecmp(base, "music") == 0 || + strcasecmp(base, "xplayer") == 0) { + return "music"; + } return NULL; } diff --git a/src/dwm-plugins/files_plugin.cpp b/src/dwm-plugins/files_plugin.cpp index f612cc0..74f6b1e 100644 --- a/src/dwm-plugins/files_plugin.cpp +++ b/src/dwm-plugins/files_plugin.cpp @@ -93,12 +93,28 @@ std::string getExtLower(const std::string& name) { } bool isImageExt(const std::string& ext) { - return ext == "jpg" || ext == "jpeg" || ext == "png"; + return ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "bmp" || + ext == "gif" || ext == "webp" || ext == "ppm" || ext == "pgm" || + ext == "pbm" || ext == "tif" || ext == "tiff" || ext == "ico" || + ext == "svg" || ext == "avif" || ext == "heic"; +} + +bool isAudioExt(const std::string& ext) { + return ext == "mp3" || ext == "wav" || ext == "ogg" || ext == "oga" || + ext == "opus" || ext == "flac" || ext == "m4a" || ext == "aac" || + ext == "wma" || ext == "aiff" || ext == "wv" || ext == "ape" || + ext == "spx" || ext == "mod" || ext == "xm" || ext == "it" || + ext == "s3m" || ext == "mptm" || ext == "stm" || ext == "med" || + ext == "okt" || ext == "669" || ext == "ult" || ext == "far" || + ext == "mtm" || ext == "ptm" || ext == "amf" || ext == "dsm" || + ext == "imf" || ext == "gdm" || ext == "stx" || ext == "umx" || + ext == "mo3" || ext == "j2b" || ext == "psm" || ext == "dbm" || + ext == "digi" || ext == "mdl" || ext == "mt2" || ext == "ams" || + ext == "plm" || ext == "sfx"; } bool isMediaExt(const std::string& ext) { - return ext == "mp3" || ext == "wav" || ext == "ogg" || ext == "flac" || - ext == "m4a" || ext == "mp4" || ext == "mkv" || ext == "webm" || + return isAudioExt(ext) || ext == "mp4" || ext == "mkv" || ext == "webm" || ext == "avi"; } @@ -311,9 +327,17 @@ int runFilesApp(int argc, char** argv) { dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0, reinterpret_cast(imgCanvas.data()), kImgCanvasW, kImgCanvasH, 32, kImgCanvasW * 4); + constexpr int kImgFullW = 240; + constexpr int kImgFullH = 270; + std::vector imgFullCanvas(kImgFullW * kImgFullH, 0x111111); + XImage* xImgFullPreview = XCreateImage( + dpy, visual, DefaultDepth(dpy, screen), ZPixmap, 0, + reinterpret_cast(imgFullCanvas.data()), kImgFullW, kImgFullH, 32, + kImgFullW * 4); int imgOrigW = 0; int imgOrigH = 0; bool imgValid = false; + bool imgFullscreen = false; // Modal input state (New Folder / New File / Rename) InputAction inputAction = InputAction::NewFolder; @@ -487,19 +511,113 @@ int runFilesApp(int argc, char** argv) { if (imgValid) { scaleRgb24ToBgrxCanvas(rgb.data(), imgOrigW, imgOrigH, imgCanvas.data(), kImgCanvasW, kImgCanvasH, 0x111111); + scaleRgb24ToBgrxCanvas(rgb.data(), imgOrigW, imgOrigH, + imgFullCanvas.data(), kImgFullW, kImgFullH, + 0x111111); } else { std::fill(imgCanvas.begin(), imgCanvas.end(), 0x111111); + std::fill(imgFullCanvas.begin(), imgFullCanvas.end(), 0x111111); } mode = ViewMode::ImageView; }; + auto installAsWallpaper = [&](const std::string& srcPath, + const std::string& fileName) { + std::string targetPath = srcPath; + std::string wallDir = homeDir + "/Pictures/Wallpapers"; + std::string picDir = homeDir + "/Pictures"; + if (srcPath.rfind("/usr/share/wallpapers/", 0) != 0 && + srcPath.rfind(wallDir + "/", 0) != 0 && + srcPath.rfind(picDir + "/", 0) != 0) { + ::mkdir(picDir.c_str(), 0755); + ::mkdir(wallDir.c_str(), 0755); + std::string dst = wallDir + "/" + fileName; + if (copyRecursive(srcPath, dst)) { + targetPath = dst; + } + } + + std::string cfgDir = homeDir + "/.config/sxmo"; + const char* xdgCfg = std::getenv("XDG_CONFIG_HOME"); + if (xdgCfg && *xdgCfg) cfgDir = std::string(xdgCfg) + "/sxmo"; + ::mkdir((homeDir + "/.config").c_str(), 0755); + ::mkdir(cfgDir.c_str(), 0755); + + { + std::ofstream ofs(cfgDir + "/wallpaper", std::ios::trunc); + if (ofs) ofs << targetPath << "\n"; + } + + std::string fehFlag = "--bg-fill"; + { + std::ifstream ifs(cfgDir + "/wallpaper_mode"); + std::string m; + if (ifs && std::getline(ifs, m)) { + m = toLowerStr(trimStr(m)); + if (m == "max" || m == "--bg-max") fehFlag = "--bg-max"; + else if (m == "scale" || m == "--bg-scale") fehFlag = "--bg-scale"; + else if (m == "center" || m == "--bg-center") fehFlag = "--bg-center"; + else if (m == "tile" || m == "--bg-tile") fehFlag = "--bg-tile"; + } + } + + runAsyncCmd("feh --no-fehbg " + fehFlag + " " + shellQuote(targetPath) + + " >/dev/null 2>&1; " + "PATH=\"$HOME/.config/sxmo/hooks:/usr/share/sxmo/default_hooks:$PATH\" " + "sxmo_hook_statusbar.sh time >/dev/null 2>&1"); + statusBanner = "Wallpaper set: " + fileName; + statusBannerUntilMs = nowMs() + 2200; + }; + + auto installAsRingtone = [&](const std::string& srcPath, + const std::string& fileName) { + std::string targetPath = srcPath; + std::string ringDir = homeDir + "/Ringtones"; + std::string musDir = homeDir + "/Music"; + if (srcPath.rfind("/usr/share/sxmo/", 0) != 0 && + srcPath.rfind(ringDir + "/", 0) != 0 && + srcPath.rfind(musDir + "/", 0) != 0) { + ::mkdir(ringDir.c_str(), 0755); + std::string dst = ringDir + "/" + fileName; + if (copyRecursive(srcPath, dst)) { + targetPath = dst; + } + } + + std::string cfgDir = homeDir + "/.config/sxmo"; + const char* xdgCfg = std::getenv("XDG_CONFIG_HOME"); + if (xdgCfg && *xdgCfg) cfgDir = std::string(xdgCfg) + "/sxmo"; + ::mkdir((homeDir + "/.config").c_str(), 0755); + ::mkdir(cfgDir.c_str(), 0755); + + { + std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc); + if (ofs) ofs << targetPath << "\n"; + } + statusBanner = "Ringtone set: " + fileName; + statusBannerUntilMs = nowMs() + 2200; + }; + auto buildActionsList = [&]() { actionsList.clear(); bool hasSel = !filteredItems.empty(); bool isParent = hasSel && filteredItems[selected].isParent; if (hasSel) { - actionsList.push_back({1, kIconEye, "Open / View"}); + if (!filteredItems[selected].isDir) { + std::string ext = getExtLower(filteredItems[selected].name); + if (isImageExt(ext)) { + actionsList.push_back({1, kIconEye, "Preview Image"}); + actionsList.push_back({14, kIconImage, "Set as Wallpaper"}); + } else if (isAudioExt(ext)) { + actionsList.push_back({1, kIconMusic, "Play in Music Player"}); + actionsList.push_back({15, kIconBell, "Set as Ringtone"}); + } else { + actionsList.push_back({1, kIconEye, "Open / View"}); + } + } else { + actionsList.push_back({1, kIconFolderOpen, "Open Folder"}); + } } if (!clipPath.empty()) { actionsList.push_back( @@ -540,6 +658,15 @@ int runFilesApp(int argc, char** argv) { ::unlink(activePidPath.c_str()); }; + auto openInMusicPlayer = [&](const std::string& path) { + hideWindow(); + if (isPluginDaemonAlive("music")) { + sendPluginCommand("music", "OPEN " + path); + } else { + runAsyncArgv("xmusic", path.c_str()); + } + }; + auto openSelectedItem = [&]() { if (filteredItems.empty()) return; const FileItem item = filteredItems[selected]; @@ -557,7 +684,11 @@ int runFilesApp(int argc, char** argv) { } std::string ext = getExtLower(item.name); if (isImageExt(ext)) { + imgFullscreen = false; openImageViewer(item); + } else if (isAudioExt(ext)) { + openInMusicPlayer(item.fullPath); + return; } else if (isMediaExt(ext)) { hideWindow(); runAsyncArgv("sxmo_open.sh", item.fullPath.c_str()); @@ -786,19 +917,25 @@ int runFilesApp(int argc, char** argv) { drawSoftkeyBar("Edit", "Down", "Back"); } else if (mode == ViewMode::ImageView) { - XSetForeground(dpy, gc, pt.spec.bgHeader); - XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); - drawPango(layoutIcon, &colGreen, 6, 1, 18, kIconImage); - drawPango(layoutMain, &colWhite, 28, 0, 20, viewingFileName, 145); - std::string dim = imgValid - ? (std::to_string(imgOrigW) + "x" + - std::to_string(imgOrigH)) - : "Decode failed"; - drawPango(layoutSmall, &colMuted, 28, 19, 15, dim, 200); + if (imgFullscreen) { + XPutImage(dpy, pix, gc, xImgFullPreview, 0, 0, 0, 0, kImgFullW, + kImgFullH); + } else { + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36); + drawPango(layoutIcon, &colGreen, 6, 1, 18, kIconImage); + drawPango(layoutMain, &colWhite, 28, 0, 20, viewingFileName, 145); + std::string dim = imgValid + ? (std::to_string(imgOrigW) + "x" + + std::to_string(imgOrigH) + " ([*] Full)") + : "Decode failed"; + drawPango(layoutSmall, &colMuted, 28, 19, 15, dim, 200); - XPutImage(dpy, pix, gc, xImgPreview, 0, 0, 0, 36, kImgCanvasW, - kImgCanvasH); - drawSoftkeyBar("Full", "Next", "Back"); + XPutImage(dpy, pix, gc, xImgPreview, 0, 0, 0, 36, kImgCanvasW, + kImgCanvasH); + } + drawSoftkeyBar("Set Wall", "Next", + imgFullscreen ? "Header" : "Back"); } else if (mode == ViewMode::PropsView && !filteredItems.empty()) { const auto& it = filteredItems[selected]; @@ -1027,6 +1164,14 @@ int runFilesApp(int argc, char** argv) { } else if (actId == 13 && !filteredItems.empty()) { confirmSelected = 0; mode = ViewMode::ConfirmDelete; + } else if (actId == 14 && !filteredItems.empty()) { + installAsWallpaper(filteredItems[selected].fullPath, + filteredItems[selected].name); + mode = ViewMode::Browser; + } else if (actId == 15 && !filteredItems.empty()) { + installAsRingtone(filteredItems[selected].fullPath, + filteredItems[selected].name); + mode = ViewMode::Browser; } }; @@ -1077,9 +1222,13 @@ int runFilesApp(int argc, char** argv) { } } else if (mode == ViewMode::ImageView) { if (isLeft) { - runAsyncArgv("feh", viewingFilePath.c_str()); + installAsWallpaper(viewingFilePath, viewingFileName); } else { - mode = ViewMode::Browser; + if (imgFullscreen) { + imgFullscreen = false; + } else { + mode = ViewMode::Browser; + } } } else if (mode == ViewMode::PropsView) { mode = ViewMode::Browser; @@ -1300,8 +1449,12 @@ int runFilesApp(int argc, char** argv) { drawUI(); } } else if (mode == ViewMode::ImageView) { - if (sym == XK_Left || sym == XK_KP_Left || sym == XK_Up || - sym == XK_KP_Up) { + char d = mapDialKey(sym, kc, ev.xkey.state); + if (d == '*') { + imgFullscreen = !imgFullscreen; + drawUI(); + } else if (sym == XK_Left || sym == XK_KP_Left || sym == XK_Up || + sym == XK_KP_Up) { stepImageInFolder(-1); drawUI(); } else if (sym == XK_Right || sym == XK_KP_Right || sym == XK_Down || @@ -1395,6 +1548,10 @@ int runFilesApp(int argc, char** argv) { xImgPreview->data = nullptr; XDestroyImage(xImgPreview); } + if (xImgFullPreview) { + xImgFullPreview->data = nullptr; + XDestroyImage(xImgFullPreview); + } if (fifoFd >= 0) ::close(fifoFd); ::unlink(daemonPidPath.c_str()); return 0; diff --git a/src/dwm-plugins/media_utils.h b/src/dwm-plugins/media_utils.h index c5ff5c2..dbdc121 100644 --- a/src/dwm-plugins/media_utils.h +++ b/src/dwm-plugins/media_utils.h @@ -14,6 +14,8 @@ #include #include +#include +#include namespace bananarchy { @@ -159,6 +161,73 @@ inline bool loadPngRgb24(const std::string& path, return true; } +inline bool loadFfmpegImageRgb24(const std::string& path, + std::vector* outRgb, + int* outW, + int* outH) { + int pipefd[2]; + if (::pipe(pipefd) != 0) { + return false; + } + pid_t pid = ::fork(); + if (pid < 0) { + ::close(pipefd[0]); + ::close(pipefd[1]); + return false; + } + if (pid == 0) { + ::close(pipefd[0]); + ::dup2(pipefd[1], STDOUT_FILENO); + if (pipefd[1] != STDOUT_FILENO) ::close(pipefd[1]); + int devnull = ::open("/dev/null", O_RDWR); + if (devnull >= 0) { + ::dup2(devnull, STDIN_FILENO); + ::dup2(devnull, STDERR_FILENO); + if (devnull > 2) ::close(devnull); + } + ::execlp("ffmpeg", "ffmpeg", "-v", "error", "-i", path.c_str(), + "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "-", + static_cast(nullptr)); + ::_exit(127); + } + ::close(pipefd[1]); + std::vector pngBuf; + uint8_t chunk[4096]; + ssize_t n = 0; + while ((n = ::read(pipefd[0], chunk, sizeof(chunk))) > 0) { + if (pngBuf.size() + static_cast(n) > 16 * 1024 * 1024) break; + pngBuf.insert(pngBuf.end(), chunk, chunk + n); + } + ::close(pipefd[0]); + int status = 0; + ::waitpid(pid, &status, 0); + if (pngBuf.empty()) { + return false; + } + + png_image image {}; + image.version = PNG_IMAGE_VERSION; + if (!png_image_begin_read_from_memory(&image, pngBuf.data(), pngBuf.size())) { + return false; + } + image.format = PNG_FORMAT_RGB; + int w = static_cast(image.width); + int h = static_cast(image.height); + if (w <= 0 || h <= 0 || w > 8192 || h > 8192) { + png_image_free(&image); + return false; + } + outRgb->resize(PNG_IMAGE_SIZE(image)); + if (!png_image_finish_read(&image, nullptr, outRgb->data(), 0, nullptr)) { + png_image_free(&image); + return false; + } + png_image_free(&image); + *outW = w; + *outH = h; + return true; +} + inline bool loadImageRgb24(const std::string& path, std::vector* outRgb, int* outW, @@ -178,6 +247,9 @@ inline bool loadImageRgb24(const std::string& path, if (loadPngRgb24(path, outRgb, outW, outH)) { return true; } + if (loadFfmpegImageRgb24(path, outRgb, outW, outH)) { + return true; + } return false; } diff --git a/src/dwm-plugins/music_plugin.cpp b/src/dwm-plugins/music_plugin.cpp new file mode 100644 index 0000000..98b51dd --- /dev/null +++ b/src/dwm-plugins/music_plugin.cpp @@ -0,0 +1,1585 @@ +#include "phone_data.h" +#include "../sxmo-dwm/dwm_plugin.h" + +#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 + +namespace { + +using namespace bananarchy; + +volatile sig_atomic_t gRunning = 1; + +void handleSignal(int) { + gRunning = 0; +} + +enum class TabId { + Player = 0, + Queue = 1, + Browser = 2, +}; + +enum class RepeatMode { + All = 0, + One = 1, + Off = 2, +}; + +struct TrackEntry { + std::string path; + std::string displayName; + std::string folderName; + std::string formatTag; + bool isTracker = false; + off_t sizeBytes = 0; +}; + +struct BrowserItem { + std::string name; + std::string fullPath; + std::string lowerName; + std::string formatTag; + bool isParent = false; + bool isDir = false; + bool isTracker = false; + off_t sizeBytes = 0; +}; + +constexpr const char* kMpvSockPath = "/tmp/xmusic_mpv.sock"; + +std::string getExtLower(const std::string& name) { + size_t dot = name.rfind('.'); + if (dot == std::string::npos || dot + 1 >= name.size()) { + return ""; + } + return toLowerStr(name.substr(dot + 1)); +} + +bool isTrackerFormatExt(const std::string& ext) { + return ext == "mod" || ext == "xm" || ext == "it" || ext == "s3m" || + ext == "mptm" || ext == "stm" || ext == "med" || ext == "okt" || + ext == "669" || ext == "ult" || ext == "far" || ext == "mtm" || + ext == "ptm" || ext == "amf" || ext == "dsm" || ext == "imf" || + ext == "gdm" || ext == "stx" || ext == "umx" || ext == "mo3" || + ext == "j2b" || ext == "psm" || ext == "dbm" || ext == "digi" || + ext == "mdl" || ext == "mt2" || ext == "ams" || ext == "plm" || + ext == "sfx"; +} + +bool isStandardAudioExt(const std::string& ext) { + return ext == "mp3" || ext == "wav" || ext == "ogg" || ext == "oga" || + ext == "opus" || ext == "flac" || ext == "m4a" || ext == "aac" || + ext == "wma" || ext == "aiff" || ext == "wv" || ext == "ape" || + ext == "spx"; +} + +bool isSupportedAudioExt(const std::string& ext) { + return isStandardAudioExt(ext) || isTrackerFormatExt(ext); +} + +std::string toUpperTag(const std::string& ext) { + std::string out = ext; + for (char& c : out) { + c = static_cast(std::toupper(static_cast(c))); + } + return out.empty() ? "AUD" : out; +} + +std::string prettifyTrackName(const std::string& filename) { + std::string base = filename; + size_t dot = base.rfind('.'); + if (dot != std::string::npos && dot > 0) { + base = base.substr(0, dot); + } + for (char& c : base) { + if (c == '_') c = ' '; + } + return base; +} + +std::string extractParentName(const std::string& path) { + size_t sl = path.rfind('/'); + if (sl == std::string::npos || sl == 0) return "/"; + std::string dir = path.substr(0, sl); + size_t sl2 = dir.rfind('/'); + if (sl2 == std::string::npos) return dir; + return dir.substr(sl2 + 1); +} + +std::string formatFileSizeShort(off_t bytes) { + char buf[32]; + if (bytes >= 1024LL * 1024LL) { + std::snprintf(buf, sizeof(buf), "%.1fM", + static_cast(bytes) / (1024.0 * 1024.0)); + } else if (bytes >= 1024LL) { + std::snprintf(buf, sizeof(buf), "%lldK", + static_cast((bytes + 512) / 1024)); + } else { + std::snprintf(buf, sizeof(buf), "%lldB", static_cast(bytes)); + } + return buf; +} + +std::string formatTimeMmSs(double sec) { + if (sec < 0.0 || !std::isfinite(sec)) sec = 0.0; + int total = static_cast(sec + 0.5); + int mins = total / 60; + int secs = total % 60; + char buf[32]; + if (mins >= 60) { + int hrs = mins / 60; + mins %= 60; + std::snprintf(buf, sizeof(buf), "%d:%02d:%02d", hrs, mins, secs); + } else { + std::snprintf(buf, sizeof(buf), "%02d:%02d", mins, secs); + } + return buf; +} + +std::string jsonEscape(const std::string& s) { + std::string out; + out.reserve(s.size() + 8); + for (char c : s) { + if (c == '\\') out += "\\\\"; + else if (c == '"') out += "\\\""; + else if (c == '\n') out += "\\n"; + else out.push_back(c); + } + return out; +} + +TrackEntry makeTrackEntry(const std::string& fullPath) { + TrackEntry t; + t.path = fullPath; + size_t sl = fullPath.rfind('/'); + std::string fname = + (sl != std::string::npos) ? fullPath.substr(sl + 1) : fullPath; + std::string ext = getExtLower(fname); + t.displayName = prettifyTrackName(fname); + t.folderName = extractParentName(fullPath); + t.formatTag = toUpperTag(ext); + t.isTracker = isTrackerFormatExt(ext); + struct stat st {}; + if (::stat(fullPath.c_str(), &st) == 0) { + t.sizeBytes = st.st_size; + } + return t; +} + +void scanTracksInDir(const std::string& dirPath, + bool recursive, + std::vector& out, + int depth = 0) { + if (depth > 4) return; + DIR* d = ::opendir(dirPath.c_str()); + if (!d) return; + + std::vector subdirs; + std::vector files; + while (struct dirent* ent = ::readdir(d)) { + if (ent->d_name[0] == '.') continue; + std::string name = ent->d_name; + std::string full = + (dirPath == "/") ? ("/" + name) : (dirPath + "/" + name); + struct stat st {}; + if (::stat(full.c_str(), &st) != 0) continue; + if (S_ISDIR(st.st_mode)) { + if (recursive) subdirs.push_back(full); + } else if (S_ISREG(st.st_mode)) { + std::string ext = getExtLower(name); + if (isSupportedAudioExt(ext)) { + files.push_back(full); + } + } + } + ::closedir(d); + + std::sort(files.begin(), files.end()); + for (const auto& f : files) { + bool exists = false; + for (const auto& e : out) { + if (e.path == f) { + exists = true; + break; + } + } + if (!exists) { + out.push_back(makeTrackEntry(f)); + } + } + + std::sort(subdirs.begin(), subdirs.end()); + for (const auto& sd : subdirs) { + scanTracksInDir(sd, recursive, out, depth + 1); + } +} + +std::string getHomePath() { + const char* h = std::getenv("HOME"); + return (h && *h) ? std::string(h) : "/home/user"; +} + +std::string getConfigDirPath() { + const char* c = std::getenv("XDG_CONFIG_HOME"); + if (c && *c) return std::string(c) + "/sxmo"; + return getHomePath() + "/.config/sxmo"; +} + +int readSavedSpeakerVol() { + std::ifstream ifs(getConfigDirPath() + "/speaker_vol"); + int v = 85; + if (ifs && (ifs >> v)) { + return std::clamp((v / 5) * 5, 0, 100); + } + return 85; +} + +void applySystemSpeakerVol(int pct) { + pct = std::clamp((pct / 5) * 5, 0, 100); + std::string cfgDir = getConfigDirPath(); + ::mkdir(cfgDir.c_str(), 0755); + { + std::ofstream ofs(cfgDir + "/speaker_vol", std::ios::trunc); + if (ofs) ofs << pct << "\n"; + } + int rx3 = (pct <= 0) ? 0 : (54 + (pct * 38) / 100); + runAsyncCmd("amixer -c 0 cset \"name='SPK DAC Switch'\" " + + std::string(pct > 0 ? "1" : "0") + " >/dev/null 2>&1; " + "amixer -c 0 cset \"name='RX3 Digital Volume'\" " + + std::to_string(rx3) + " >/dev/null 2>&1; " + "wpctl set-mute @DEFAULT_AUDIO_SINK@ " + + std::string(pct == 0 ? "1" : "0") + " >/dev/null 2>&1; " + "wpctl set-volume @DEFAULT_AUDIO_SINK@ " + + std::to_string(pct) + "% >/dev/null 2>&1; " + "sxmo_hook_statusbar.sh audio >/dev/null 2>&1"); +} + +int runMusicApp(int argc, char** argv) { + gRunning = 1; + std::setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + + bool isPreload = false; + std::string initTarget; + for (int i = 1; i < argc; ++i) { + if (!argv[i]) continue; + if (std::strcmp(argv[i], "--preload") == 0) { + isPreload = true; + } else if (argv[i][0] != '-' && initTarget.empty()) { + initTarget = argv[i]; + } + } + + std::string daemonPidPath = getPluginDaemonPidPath("music"); + std::string activePidPath = getRuntimeFile("xmusic.pid"); + + if (isPluginDaemonAlive("music")) { + if (!isPreload) { + sendPluginCommand( + "music", initTarget.empty() ? "OPEN" : ("OPEN " + initTarget)); + } + return 0; + } + + writeIntFile(daemonPidPath, static_cast(::getpid())); + int fifoFd = setupPluginDaemonFifo("music"); + + struct sigaction sa {}; + sa.sa_handler = handleSignal; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) { + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + return 1; + } + + int screen = DefaultScreen(dpy); + Window root = RootWindow(dpy, screen); + Visual* visual = DefaultVisual(dpy, screen); + Colormap cmap = DefaultColormap(dpy, screen); + + ThemeSpec initSpec; + initSpec.loadActive(dpy, screen); + + XSetWindowAttributes swa {}; + swa.override_redirect = True; + swa.background_pixel = initSpec.bgMain; + swa.border_pixel = initSpec.selBg; + swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | + VisibilityChangeMask | StructureNotifyMask; + + Window win = XCreateWindow( + dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, + DefaultDepth(dpy, screen), CopyFromParent, visual, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); + + XClassHint ch {}; + char resName[] = "xmusic"; + char resClass[] = "XMusic"; + ch.res_name = resName; + ch.res_class = resClass; + XSetClassHint(dpy, win, &ch); + XStoreName(dpy, win, "Music"); + + Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, + DefaultDepth(dpy, screen)); + GC gc = XCreateGC(dpy, pix, 0, nullptr); + XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); + + PluginTheme pt; + pt.init(dpy, screen, visual, cmap, win); + auto& layoutMain = pt.layoutMain; + auto& layoutSmall = pt.layoutSmall; + auto& layoutIcon = pt.layoutIcon; + auto& colWhite = pt.colWhite; + auto& colLight = pt.colLight; + auto& colMuted = pt.colMuted; + auto& colYellow = pt.colYellow; + auto& colGreen = pt.colGreen; + auto& colBlue = pt.colBlue; + auto& colOrange = pt.colOrange; + + std::string homeDir = getHomePath(); + std::string musicRoot = homeDir + "/Music"; + ::mkdir(musicRoot.c_str(), 0755); + + TabId activeTab = TabId::Player; + std::vector queue; + int currentTrackIdx = -1; + int queueSelIdx = 0; + + std::string browserDir = musicRoot; + std::vector browserItems; + int browserSelIdx = 0; + + RepeatMode repeatMode = RepeatMode::All; + bool shuffleOn = false; + int volumePct = readSavedSpeakerVol(); + + // Playback state from mpv IPC + pid_t mpvPid = -1; + int ipcFd = -1; + std::string ipcReadBuf; + std::string pendingLoadPath; + bool isPlaying = false; + bool isPaused = false; + double timePosSec = 0.0; + double durationSec = 0.0; + std::string mediaTitle; + int64_t ignoreEofUntilMs = 0; + + std::string statusBanner; + int64_t statusBannerUntilMs = 0; + + bool visible = false; + int64_t startupIgnoreUntilMs = 0; + bool initialSoftkeysReleased = true; + int softkeyPending = 0; + int64_t softkeyDeadlineMs = 0; + + std::mt19937 rng(static_cast(nowMs())); + + auto setBanner = [&](const std::string& msg, int durationMs = 2000) { + statusBanner = msg; + statusBannerUntilMs = nowMs() + durationMs; + }; + + auto sendIpcRaw = [&](const std::string& jsonLine) -> bool { + if (ipcFd < 0) return false; + std::string msg = jsonLine + "\n"; + ssize_t w = ::write(ipcFd, msg.data(), msg.size()); + return w == static_cast(msg.size()); + }; + + auto stopMpvDaemon = [&]() { + if (ipcFd >= 0) { + ::close(ipcFd); + ipcFd = -1; + } + ipcReadBuf.clear(); + if (mpvPid > 0) { + ::kill(mpvPid, SIGTERM); + ::waitpid(mpvPid, nullptr, WNOHANG); + mpvPid = -1; + } + ::unlink(kMpvSockPath); + }; + + auto ensureMpvRunning = [&]() { + if (mpvPid > 0) { + int st = 0; + pid_t r = ::waitpid(mpvPid, &st, WNOHANG); + if (r == mpvPid || (r < 0 && errno == ECHILD)) { + mpvPid = -1; + if (ipcFd >= 0) { + ::close(ipcFd); + ipcFd = -1; + } + } + } + if (mpvPid <= 0) { + ::unlink(kMpvSockPath); + pid_t pid = ::fork(); + if (pid == 0) { + ::setsid(); + int devNull = ::open("/dev/null", O_RDWR); + if (devNull >= 0) { + ::dup2(devNull, STDIN_FILENO); + ::dup2(devNull, STDOUT_FILENO); + ::dup2(devNull, STDERR_FILENO); + if (devNull > 2) ::close(devNull); + } + std::string ipcArg = + std::string("--input-ipc-server=") + kMpvSockPath; + ::execlp("mpv", "mpv", "--no-video", "--no-terminal", "--quiet", + "--idle=yes", "--keep-open=no", "--volume-max=150", + "--volume=100", ipcArg.c_str(), + static_cast(nullptr)); + ::_exit(127); + } else if (pid > 0) { + mpvPid = pid; + } + } + }; + + auto tryConnectIpc = [&]() -> bool { + if (ipcFd >= 0) return true; + int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) return false; + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + std::snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", kMpvSockPath); + if (::connect(fd, reinterpret_cast(&addr), + sizeof(addr)) != 0) { + ::close(fd); + return false; + } + ipcFd = fd; + ipcReadBuf.clear(); + sendIpcRaw("{\"command\":[\"observe_property\",1,\"time-pos\"]}"); + sendIpcRaw("{\"command\":[\"observe_property\",2,\"duration\"]}"); + sendIpcRaw("{\"command\":[\"observe_property\",3,\"pause\"]}"); + sendIpcRaw("{\"command\":[\"observe_property\",4,\"media-title\"]}"); + if (!pendingLoadPath.empty()) { + std::string p = pendingLoadPath; + pendingLoadPath.clear(); + ignoreEofUntilMs = nowMs() + 450; + sendIpcRaw("{\"command\":[\"loadfile\",\"" + jsonEscape(p) + + "\",\"replace\"]}"); + sendIpcRaw("{\"command\":[\"set_property\",\"pause\",false]}"); + } + return true; + }; + + auto playTrackAtIndex = [&](int idx) { + if (queue.empty() || idx < 0 || idx >= static_cast(queue.size())) { + return; + } + currentTrackIdx = idx; + queueSelIdx = idx; + const auto& tr = queue[idx]; + timePosSec = 0.0; + durationSec = 0.0; + mediaTitle = tr.displayName; + isPlaying = true; + isPaused = false; + ignoreEofUntilMs = nowMs() + 450; + + runAsyncCmd("amixer -c 0 cset \"name='SPK DAC Switch'\" 1 >/dev/null 2>&1; " + "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1"); + + ensureMpvRunning(); + if (tryConnectIpc()) { + sendIpcRaw("{\"command\":[\"loadfile\",\"" + jsonEscape(tr.path) + + "\",\"replace\"]}"); + sendIpcRaw("{\"command\":[\"set_property\",\"pause\",false]}"); + } else { + pendingLoadPath = tr.path; + } + }; + + auto stopPlayback = [&]() { + pendingLoadPath.clear(); + isPlaying = false; + isPaused = false; + timePosSec = 0.0; + durationSec = 0.0; + ignoreEofUntilMs = nowMs() + 400; + if (ipcFd >= 0) { + sendIpcRaw("{\"command\":[\"stop\"]}"); + } + }; + + auto togglePlayPause = [&]() { + if (queue.empty()) { + setBanner("Queue is empty"); + return; + } + if (!isPlaying) { + int target = (currentTrackIdx >= 0 && + currentTrackIdx < static_cast(queue.size())) + ? currentTrackIdx + : queueSelIdx; + playTrackAtIndex(std::clamp(target, 0, + static_cast(queue.size()) - 1)); + return; + } + isPaused = !isPaused; + if (tryConnectIpc()) { + sendIpcRaw(std::string("{\"command\":[\"set_property\",\"pause\",") + + (isPaused ? "true" : "false") + "]}"); + } + }; + + auto pausePlayback = [&]() { + if (isPlaying && !isPaused) { + isPaused = true; + if (tryConnectIpc()) { + sendIpcRaw("{\"command\":[\"set_property\",\"pause\",true]}"); + } + } + }; + + auto seekRelative = [&](int deltaSec) { + if (!isPlaying || ipcFd < 0) return; + sendIpcRaw("{\"command\":[\"seek\"," + std::to_string(deltaSec) + + ",\"relative\"]}"); + timePosSec = std::max(0.0, timePosSec + deltaSec); + if (durationSec > 0.0) { + timePosSec = std::min(durationSec, timePosSec); + } + }; + + auto stepTrack = [&](int delta, bool autoAdvance = false) { + if (queue.empty()) return; + int total = static_cast(queue.size()); + if (autoAdvance && repeatMode == RepeatMode::One && currentTrackIdx >= 0) { + playTrackAtIndex(currentTrackIdx); + return; + } + if (shuffleOn && total > 1) { + std::uniform_int_distribution dist(0, total - 2); + int r = dist(rng); + if (r >= currentTrackIdx) ++r; + playTrackAtIndex(r); + return; + } + int base = (currentTrackIdx >= 0) ? currentTrackIdx : queueSelIdx; + int next = base + delta; + if (next >= total) { + if (autoAdvance && repeatMode == RepeatMode::Off) { + stopPlayback(); + return; + } + next = 0; + } else if (next < 0) { + next = total - 1; + } + playTrackAtIndex(next); + }; + + auto loadBrowserDirectory = [&](const std::string& targetDir, + const std::string& selectName = "") { + char resolved[4096]; + std::string dir = targetDir; + if (::realpath(targetDir.c_str(), resolved) != nullptr) { + dir = resolved; + } + DIR* d = ::opendir(dir.c_str()); + if (!d) { + setBanner("Cannot open folder"); + return; + } + browserDir = dir; + browserItems.clear(); + + if (browserDir != "/") { + BrowserItem up; + up.name = ".. (Up)"; + size_t sl = browserDir.rfind('/'); + up.fullPath = (sl == 0 || sl == std::string::npos) + ? "/" + : browserDir.substr(0, sl); + up.lowerName = ".."; + up.isParent = true; + up.isDir = true; + browserItems.push_back(up); + } + + std::vector dirs; + std::vector files; + while (struct dirent* ent = ::readdir(d)) { + if (ent->d_name[0] == '.') continue; + std::string name = ent->d_name; + std::string full = + (browserDir == "/") ? ("/" + name) : (browserDir + "/" + name); + struct stat st {}; + if (::stat(full.c_str(), &st) != 0) continue; + + BrowserItem item; + item.name = name; + item.fullPath = full; + item.lowerName = toLowerStr(name); + item.sizeBytes = st.st_size; + if (S_ISDIR(st.st_mode)) { + item.isDir = true; + item.formatTag = "DIR"; + dirs.push_back(item); + } else if (S_ISREG(st.st_mode)) { + std::string ext = getExtLower(name); + if (isSupportedAudioExt(ext)) { + item.isTracker = isTrackerFormatExt(ext); + item.formatTag = toUpperTag(ext); + files.push_back(item); + } + } + } + ::closedir(d); + + auto cmp = [](const BrowserItem& a, const BrowserItem& b) { + return a.lowerName < b.lowerName; + }; + std::sort(dirs.begin(), dirs.end(), cmp); + std::sort(files.begin(), files.end(), cmp); + + browserItems.insert(browserItems.end(), dirs.begin(), dirs.end()); + browserItems.insert(browserItems.end(), files.begin(), files.end()); + + browserSelIdx = 0; + if (!selectName.empty()) { + for (size_t i = 0; i < browserItems.size(); ++i) { + if (browserItems[i].name == selectName) { + browserSelIdx = static_cast(i); + break; + } + } + } + }; + + auto populateDefaultQueue = [&]() { + queue.clear(); + scanTracksInDir(musicRoot, true, queue); + scanTracksInDir("/usr/share/sxmo/music", true, queue); + if (queue.empty()) { + scanTracksInDir("/usr/share/sxmo/ringtones", false, queue); + } + if (queueSelIdx >= static_cast(queue.size())) { + queueSelIdx = std::max(0, static_cast(queue.size()) - 1); + } + }; + + auto openTargetPath = [&](const std::string& rawTarget) { + if (rawTarget.empty()) return; + char resolved[4096]; + std::string target = rawTarget; + if (::realpath(rawTarget.c_str(), resolved) != nullptr) { + target = resolved; + } + struct stat st {}; + if (::stat(target.c_str(), &st) != 0) { + setBanner("File not found"); + return; + } + if (S_ISDIR(st.st_mode)) { + std::vector dirTracks; + scanTracksInDir(target, true, dirTracks); + loadBrowserDirectory(target); + if (!dirTracks.empty()) { + queue = dirTracks; + activeTab = TabId::Player; + playTrackAtIndex(0); + } else { + activeTab = TabId::Browser; + setBanner("No audio files in folder"); + } + return; + } + + // Single audio/tracker file: load sibling audio files in the same folder + size_t sl = target.rfind('/'); + std::string parentDir = + (sl == 0 || sl == std::string::npos) ? "/" : target.substr(0, sl); + std::string fname = + (sl != std::string::npos) ? target.substr(sl + 1) : target; + + std::vector folderTracks; + scanTracksInDir(parentDir, false, folderTracks); + int foundIdx = -1; + for (size_t i = 0; i < folderTracks.size(); ++i) { + if (folderTracks[i].path == target) { + foundIdx = static_cast(i); + break; + } + } + if (foundIdx < 0) { + folderTracks.insert(folderTracks.begin(), makeTrackEntry(target)); + foundIdx = 0; + } + queue = folderTracks; + loadBrowserDirectory(parentDir, fname); + activeTab = TabId::Player; + playTrackAtIndex(foundIdx); + }; + + auto installTrackAsRingtone = [&](const TrackEntry& tr) { + std::string ringDir = homeDir + "/Ringtones"; + ::mkdir(ringDir.c_str(), 0755); + std::string targetPath = tr.path; + size_t sl = tr.path.rfind('/'); + std::string fname = + (sl != std::string::npos) ? tr.path.substr(sl + 1) : tr.path; + if (tr.path.rfind("/usr/share/sxmo/", 0) != 0 && + tr.path.rfind(ringDir + "/", 0) != 0 && + tr.path.rfind(musicRoot + "/", 0) != 0) { + std::string dst = ringDir + "/" + fname; + pid_t pid = ::fork(); + if (pid == 0) { + ::execlp("cp", "cp", "-f", tr.path.c_str(), dst.c_str(), + static_cast(nullptr)); + ::_exit(127); + } else if (pid > 0) { + int st = 0; + ::waitpid(pid, &st, 0); + if (WIFEXITED(st) && WEXITSTATUS(st) == 0) { + targetPath = dst; + } + } + } + std::string cfgDir = getConfigDirPath(); + ::mkdir(cfgDir.c_str(), 0755); + std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc); + if (ofs) ofs << targetPath << "\n"; + setBanner("Ringtone set: " + tr.displayName); + }; + + auto parseIpcLine = [&](const std::string& line) { + if (line.find("\"event\":\"end-file\"") != std::string::npos) { + if (line.find("\"reason\":\"eof\"") != std::string::npos && + nowMs() >= ignoreEofUntilMs && isPlaying) { + stepTrack(1, true); + } + return; + } + if (line.find("\"event\":\"property-change\"") == std::string::npos) { + return; + } + if (line.find("\"name\":\"time-pos\"") != std::string::npos) { + size_t dp = line.find("\"data\":"); + if (dp != std::string::npos) { + const char* p = line.c_str() + dp + 7; + if (std::strncmp(p, "null", 4) != 0) { + timePosSec = std::strtod(p, nullptr); + } + } + } else if (line.find("\"name\":\"duration\"") != std::string::npos) { + size_t dp = line.find("\"data\":"); + if (dp != std::string::npos) { + const char* p = line.c_str() + dp + 7; + if (std::strncmp(p, "null", 4) != 0) { + durationSec = std::strtod(p, nullptr); + } + } + } else if (line.find("\"name\":\"pause\"") != std::string::npos) { + if (line.find("\"data\":true") != std::string::npos) { + isPaused = true; + } else if (line.find("\"data\":false") != std::string::npos) { + isPaused = false; + } + } else if (line.find("\"name\":\"media-title\"") != std::string::npos) { + size_t dp = line.find("\"data\":\""); + if (dp != std::string::npos) { + size_t start = dp + 8; + size_t end = line.find('"', start); + if (end != std::string::npos && end > start) { + std::string t = line.substr(start, end - start); + if (!t.empty()) { + mediaTitle = prettifyTrackName(t); + } + } + } + } + }; + + auto drainIpcSocket = [&]() -> bool { + if (ipcFd < 0) return false; + bool changed = false; + char buf[1024]; + while (true) { + ssize_t n = ::read(ipcFd, buf, sizeof(buf)); + if (n > 0) { + ipcReadBuf.append(buf, static_cast(n)); + size_t nl = 0; + while ((nl = ipcReadBuf.find('\n')) != std::string::npos) { + std::string line = ipcReadBuf.substr(0, nl); + ipcReadBuf.erase(0, nl + 1); + parseIpcLine(line); + changed = true; + } + } else if (n == 0) { + ::close(ipcFd); + ipcFd = -1; + ipcReadBuf.clear(); + break; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) break; + ::close(ipcFd); + ipcFd = -1; + ipcReadBuf.clear(); + break; + } + } + return changed; + }; + + auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, + int h, const std::string& text, int maxW = -1, + int align = 0) { + if (text.empty()) return; + pango_layout_set_text(layout, text.c_str(), -1); + if (maxW > 0) { + pango_layout_set_width(layout, maxW * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } else { + pango_layout_set_width(layout, -1); + } + int tw = 0, th = 0; + pango_layout_get_pixel_size(layout, &tw, &th); + int tx = x; + if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2; + else if (align == 2 && maxW > 0) tx = x + maxW - tw; + int ty = y + (h - th) / 2; + pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE, + ty * PANGO_SCALE); + }; + + auto drawSoftkeyBar = [&](const std::string& leftLabel, + const std::string& midLabel, + const std::string& rightLabel) { + int barY = kWinHeight - 24; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24); + XSetForeground(dpy, gc, pt.spec.border); + XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY); + + drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0); + drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1); + drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2); + }; + + auto render = [&]() { + if (!visible) return; + pt.checkAndReload(); + + XSetForeground(dpy, gc, pt.spec.bgMain); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); + + // Top 3-tab header (0..26) + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26); + + const char* tabNames[3] = {"1:Player", "2:Queue", "3:Files"}; + for (int i = 0; i < 3; ++i) { + int tx = i * 80; + bool isAct = (static_cast(activeTab) == i); + if (isAct) { + XSetForeground(dpy, gc, pt.spec.selBg); + XFillRectangle(dpy, pix, gc, tx, 0, 80, 26); + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, tx, 24, 80, 2); + } + drawPango(layoutSmall, isAct ? &colWhite : &colMuted, tx + 2, 0, 24, + tabNames[i], 76, 1); + } + + if (activeTab == TabId::Player) { + // Track info card (y=30..104) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 30, kWinWidth - 8, 74); + XSetForeground(dpy, gc, pt.spec.border); + XDrawRectangle(dpy, pix, gc, 4, 30, kWinWidth - 9, 73); + + bool hasTrack = (currentTrackIdx >= 0 && + currentTrackIdx < static_cast(queue.size())); + const TrackEntry* cur = + hasTrack ? &queue[currentTrackIdx] + : (!queue.empty() && queueSelIdx >= 0 && + queueSelIdx < static_cast(queue.size()) + ? &queue[queueSelIdx] + : nullptr); + + if (cur) { + std::string badge = + std::string("[") + cur->formatTag + + (cur->isTracker ? " TRACKER]" : "]"); + drawPango(layoutSmall, cur->isTracker ? &colOrange : &colBlue, 10, 33, + 18, badge, 130, 0); + + int dispIdx = hasTrack ? (currentTrackIdx + 1) : (queueSelIdx + 1); + std::string idxStr = + std::to_string(dispIdx) + " / " + std::to_string(queue.size()); + drawPango(layoutSmall, &colMuted, 140, 33, 18, idxStr, 90, 2); + + std::string titleStr = + (isPlaying && !mediaTitle.empty()) ? mediaTitle : cur->displayName; + drawPango(layoutMain, &colWhite, 10, 53, 24, + std::string(kIconMusic) + " " + titleStr, kWinWidth - 20, 0); + + std::string subStr = + cur->folderName + " | " + formatFileSizeShort(cur->sizeBytes); + drawPango(layoutSmall, &colMuted, 10, 80, 18, subStr, kWinWidth - 20, + 0); + } else { + drawPango(layoutMain, &colMuted, 10, 48, 24, "No tracks loaded", + kWinWidth - 20, 1); + drawPango(layoutSmall, &colMuted, 10, 72, 18, + "Press 3 to browse files", kWinWidth - 20, 1); + } + + // Progress bar & time card (y=108..154) + XSetForeground(dpy, gc, pt.spec.bgAltOdd); + XFillRectangle(dpy, pix, gc, 4, 108, kWinWidth - 8, 46); + + int barX = 12; + int barY = 116; + int barW = kWinWidth - 24; + int barH = 10; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, barX, barY, barW, barH); + XSetForeground(dpy, gc, pt.spec.border); + XDrawRectangle(dpy, pix, gc, barX, barY, barW - 1, barH - 1); + + double frac = 0.0; + if (durationSec > 0.1 && timePosSec >= 0.0) { + frac = std::clamp(timePosSec / durationSec, 0.0, 1.0); + } + int fillW = static_cast((barW - 2) * frac); + if (fillW > 0) { + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, barX + 1, barY + 1, fillW, barH - 2); + } + + drawPango(layoutSmall, &colLight, 12, 130, 20, + formatTimeMmSs(timePosSec), 68, 0); + + const char* stTxt = "STOPPED"; + XftColor* stCol = &colMuted; + if (isPlaying) { + if (isPaused) { + stTxt = "PAUSED"; + stCol = &colYellow; + } else { + stTxt = "PLAYING"; + stCol = &colGreen; + } + } + drawPango(layoutSmall, stCol, 80, 130, 20, stTxt, 80, 1); + + drawPango(layoutSmall, &colLight, 160, 130, 20, + durationSec > 0.1 ? formatTimeMmSs(durationSec) : "--:--", 68, + 2); + + // Mode pills row (y=158..180) + const char* repLbl = + (repeatMode == RepeatMode::All) + ? "Rep:All" + : (repeatMode == RepeatMode::One ? "Rep:One" : "Rep:Off"); + std::string volPill = "Vol:" + std::to_string(volumePct) + "%"; + std::string shufPill = std::string("Shuf:") + (shuffleOn ? "On" : "Off"); + + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 158, 74, 22); + XFillRectangle(dpy, pix, gc, 83, 158, 74, 22); + XFillRectangle(dpy, pix, gc, 162, 158, 74, 22); + + drawPango(layoutSmall, &colYellow, 6, 158, 22, volPill, 70, 1); + drawPango(layoutSmall, + repeatMode != RepeatMode::Off ? &colGreen : &colMuted, 85, 158, + 22, repLbl, 70, 1); + drawPango(layoutSmall, shuffleOn ? &colBlue : &colMuted, 164, 158, 22, + shufPill, 70, 1); + + // Keypad transport guide (y=184..268) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 184, kWinWidth - 8, 84); + drawPango(layoutSmall, &colLight, 10, 186, 19, "[OK/5] Play/Pause", 112); + drawPango(layoutSmall, &colLight, 126, 186, 19, "[*] Stop", 104); + drawPango(layoutSmall, &colMuted, 10, 206, 19, "[L/R] Seek -/+5s", 112); + drawPango(layoutSmall, &colMuted, 126, 206, 19, "[4/6] Prev/Next", 104); + drawPango(layoutSmall, &colMuted, 10, 226, 19, "[U/D] Vol -/+5%", 112); + drawPango(layoutSmall, &colMuted, 126, 226, 19, "[7/9] Seek -/+30s", 104); + drawPango(layoutSmall, &colMuted, 10, 246, 19, "[0] Repeat Mode", 112); + drawPango(layoutSmall, &colMuted, 126, 246, 19, "[#] Shuffle", 104); + + drawSoftkeyBar("Queue", + (isPlaying && !isPaused) ? "Pause" : "Play", "Hide"); + + } else if (activeTab == TabId::Queue) { + // Queue summary banner (y=28..50) + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 22); + std::string qInfo = "Tracks: " + std::to_string(queue.size()) + + " ([*] Set Ring, [0] Rescan)"; + drawPango(layoutSmall, &colBlue, 8, 28, 22, qInfo, kWinWidth - 16); + + constexpr int kVisibleRows = 5; + int total = static_cast(queue.size()); + if (total == 0) { + drawPango(layoutMain, &colMuted, 12, 110, 24, "Queue is empty", + kWinWidth - 24, 1); + drawPango(layoutSmall, &colMuted, 12, 138, 20, + "Press 0 to scan ~/Music or 3 for Files", kWinWidth - 24, 1); + } else { + int startIdx = std::clamp(queueSelIdx - kVisibleRows / 2, 0, + std::max(0, total - kVisibleRows)); + for (int i = 0; i < kVisibleRows; ++i) { + int idx = startIdx + i; + if (idx >= total) break; + const auto& tr = queue[idx]; + int ry = 54 + i * 38; + bool isSel = (idx == queueSelIdx); + bool isCur = (idx == currentTrackIdx && isPlaying); + + XSetForeground(dpy, gc, + isSel ? pt.spec.selBg + : (i % 2 == 0 ? pt.spec.bgAltEven + : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 36); + if (isCur) { + XSetForeground(dpy, gc, pt.spec.accent); + XFillRectangle(dpy, pix, gc, 4, ry, 3, 36); + } + + std::string line1 = + std::to_string(idx + 1) + ". " + tr.displayName; + drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 1, 19, + line1, 152); + + if (isCur) { + drawPango(layoutSmall, isPaused ? &colYellow : &colGreen, 162, + ry + 2, 18, isPaused ? "[PAUSED]" : "[PLAYING]", 70, 2); + } + + std::string sub = tr.formatTag + " | " + + formatFileSizeShort(tr.sizeBytes) + " | " + + tr.folderName; + drawPango(layoutSmall, isSel ? &colLight : &colMuted, 10, ry + 19, + 16, sub, kWinWidth - 20); + } + } + drawSoftkeyBar("Play", "Play", "Hide"); + + } else { + // Tab 3: Browser + XSetForeground(dpy, gc, pt.spec.bgAltEven); + XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 22); + std::string shortDir = browserDir; + if (shortDir == homeDir) shortDir = "~"; + else if (shortDir.rfind(homeDir + "/", 0) == 0) { + shortDir = "~" + shortDir.substr(homeDir.size()); + } + drawPango(layoutSmall, &colYellow, 8, 28, 22, + std::string(kIconFolderOpen) + " " + shortDir, + kWinWidth - 16); + + constexpr int kVisibleRows = 5; + int total = static_cast(browserItems.size()); + if (total == 0) { + drawPango(layoutMain, &colMuted, 12, 120, 24, "No audio files here", + kWinWidth - 24, 1); + } else { + int startIdx = std::clamp(browserSelIdx - kVisibleRows / 2, 0, + std::max(0, total - kVisibleRows)); + for (int i = 0; i < kVisibleRows; ++i) { + int idx = startIdx + i; + if (idx >= total) break; + const auto& it = browserItems[idx]; + int ry = 54 + i * 38; + bool isSel = (idx == browserSelIdx); + + XSetForeground(dpy, gc, + isSel ? pt.spec.selBg + : (i % 2 == 0 ? pt.spec.bgAltEven + : pt.spec.bgAltOdd)); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 36); + + const char* ic = it.isParent + ? kIconBackspace + : (it.isDir ? kIconFolder : kIconMusic); + XftColor* icCol = + it.isDir ? &colYellow : (it.isTracker ? &colOrange : &colBlue); + drawPango(layoutIcon, icCol, 8, ry + 2, 18, ic); + + std::string disp = + it.isDir && !it.isParent ? (it.name + "/") : it.name; + drawPango(layoutMain, isSel ? &colWhite : &colLight, 26, ry + 1, 19, + disp, 198); + + std::string sub = + it.isParent + ? "Parent directory" + : (it.isDir ? "Folder (OK to open, LSK to play all)" + : (it.formatTag + " | " + + formatFileSizeShort(it.sizeBytes))); + drawPango(layoutSmall, isSel ? &colLight : &colMuted, 26, ry + 19, + 16, sub, kWinWidth - 34); + } + } + drawSoftkeyBar("Play All", "Open", "Hide"); + } + + if (!statusBanner.empty() && nowMs() < statusBannerUntilMs) { + int by = kWinHeight - 46; + XSetForeground(dpy, gc, pt.spec.bgHeader); + XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20); + XSetForeground(dpy, gc, pt.spec.accent); + XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19); + drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner, + kWinWidth - 20, 1); + } + + XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); + XFlush(dpy); + }; + + auto showWindow = [&](const std::string& targetArg = "") { + writeIntFile(activePidPath, static_cast(::getpid())); + closeOtherPlugins("music"); + volumePct = readSavedSpeakerVol(); + if (queue.empty()) { + populateDefaultQueue(); + } + if (browserItems.empty()) { + loadBrowserDirectory(browserDir); + } + if (!targetArg.empty()) { + openTargetPath(targetArg); + } + if (!visible) { + visible = true; + XMapRaised(dpy, win); + XSync(dpy, False); + } else { + XRaiseWindow(dpy, win); + } + grabPluginKeyboard(dpy, win); + startupIgnoreUntilMs = nowMs() + 180; + initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0); + softkeyPending = 0; + render(); + }; + + auto hideWindow = [&]() { + if (!visible) return; + int xfdLocal = ConnectionNumber(dpy); + int64_t drainDeadline = nowMs() + 180; + while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) { + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + } + struct pollfd pfd {}; + pfd.fd = xfdLocal; + pfd.events = POLLIN; + ::poll(&pfd, 1, 10); + } + writeSoftkeySuppress(nowMs() + 450); + XUngrabKeyboard(dpy, CurrentTime); + visible = false; + softkeyPending = 0; + XUnmapWindow(dpy, win); + ::unlink(activePidPath.c_str()); + XFlush(dpy); + }; + + auto handleBrowserSelect = [&](bool playFolderAll) { + if (browserItems.empty()) return; + const BrowserItem item = browserItems[browserSelIdx]; + if (playFolderAll) { + std::string dirToPlay = + (item.isDir && !item.isParent) ? item.fullPath : browserDir; + std::vector tracks; + scanTracksInDir(dirToPlay, true, tracks); + if (tracks.empty()) { + setBanner("No audio files found"); + return; + } + queue = tracks; + activeTab = TabId::Player; + playTrackAtIndex(0); + setBanner("Playing " + std::to_string(queue.size()) + " tracks"); + return; + } + if (item.isParent) { + std::string prevBase = browserDir; + size_t sl = prevBase.rfind('/'); + std::string prevName = + (sl != std::string::npos) ? prevBase.substr(sl + 1) : ""; + loadBrowserDirectory(item.fullPath, prevName); + return; + } + if (item.isDir) { + loadBrowserDirectory(item.fullPath); + return; + } + openTargetPath(item.fullPath); + }; + + if (!isPreload) { + showWindow(initTarget); + } + + int xfd = ConnectionNumber(dpy); + while (gRunning) { + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == Expose && ev.xexpose.count == 0) { + render(); + } else if (ev.type == VisibilityNotify && visible && + ev.xvisibility.state != VisibilityUnobscured) { + if (isAnotherPluginActive("music")) { + hideWindow(); + } else { + XRaiseWindow(dpy, win); + grabPluginKeyboard(dpy, win); + render(); + } + } else if (ev.type == KeyRelease) { + if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) { + initialSoftkeysReleased = true; + } + } else if (ev.type == KeyPress && visible) { + int64_t now = nowMs(); + if (now < startupIgnoreUntilMs) continue; + + KeySym sym = XLookupKeysym(&ev.xkey, 0); + unsigned int kc = ev.xkey.keycode; + + bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu || + sym == XF86XK_TaskPane || sym == XF86XK_KbdLcdMenu1 || + sym == _EVDEVK(0x246)); + bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back || + sym == XK_Escape || sym == XF86XK_KbdLcdMenu2 || + sym == _EVDEVK(0x247)); + + if (isLSK || isRSK) { + if (!initialSoftkeysReleased) continue; + int mask = querySoftkeysDown(dpy); + if (mask == 3 || + (softkeyPending != 0 && + ((isLSK && softkeyPending == 2) || + (isRSK && softkeyPending == 1)))) { + softkeyPending = 0; + writeSoftkeySuppress(now + 400); + hideWindow(); + sendPluginCommand("xdrawer", "TOGGLE"); + continue; + } + softkeyPending = isLSK ? 1 : 2; + softkeyDeadlineMs = now + 90; + continue; + } + + if (sym == XF86XK_HangupPhone || sym == XK_Cancel || kc == 124 || + sym == XF86XK_PowerOff) { + hideWindow(); + continue; + } + + char dch = mapDialKey(sym, kc, ev.xkey.state); + if (dch == '1') { + activeTab = TabId::Player; + render(); + continue; + } else if (dch == '2') { + activeTab = TabId::Queue; + render(); + continue; + } else if (dch == '3') { + activeTab = TabId::Browser; + if (browserItems.empty()) loadBrowserDirectory(browserDir); + render(); + continue; + } + + if (activeTab == TabId::Player) { + if (sym == XK_Return || sym == XK_KP_Enter || dch == '5') { + togglePlayPause(); + } else if (sym == XK_Left || sym == XK_KP_Left) { + seekRelative(-5); + } else if (sym == XK_Right || sym == XK_KP_Right) { + seekRelative(5); + } else if (sym == XK_Up || sym == XK_KP_Up) { + volumePct = std::clamp(volumePct + 5, 0, 100); + applySystemSpeakerVol(volumePct); + setBanner("Volume: " + std::to_string(volumePct) + "%", 1200); + } else if (sym == XK_Down || sym == XK_KP_Down) { + volumePct = std::clamp(volumePct - 5, 0, 100); + applySystemSpeakerVol(volumePct); + setBanner("Volume: " + std::to_string(volumePct) + "%", 1200); + } else if (dch == '4') { + stepTrack(-1); + } else if (dch == '6') { + stepTrack(1); + } else if (dch == '7') { + seekRelative(-30); + } else if (dch == '9') { + seekRelative(30); + } else if (dch == '*') { + stopPlayback(); + setBanner("Playback stopped"); + } else if (dch == '0') { + repeatMode = static_cast( + (static_cast(repeatMode) + 1) % 3); + const char* rl = + (repeatMode == RepeatMode::All) + ? "Repeat: All" + : (repeatMode == RepeatMode::One ? "Repeat: One" + : "Repeat: Off"); + setBanner(rl, 1400); + } else if (dch == '#') { + shuffleOn = !shuffleOn; + setBanner(std::string("Shuffle: ") + (shuffleOn ? "On" : "Off"), + 1400); + } + render(); + } else if (activeTab == TabId::Queue) { + if (sym == XK_Up || sym == XK_KP_Up) { + if (!queue.empty()) { + queueSelIdx = (queueSelIdx + static_cast(queue.size()) - 1) % + static_cast(queue.size()); + } + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (!queue.empty()) { + queueSelIdx = (queueSelIdx + 1) % static_cast(queue.size()); + } + } else if (sym == XK_Left || sym == XK_KP_Left) { + if (!queue.empty()) { + queueSelIdx = std::max(0, queueSelIdx - 5); + } + } else if (sym == XK_Right || sym == XK_KP_Right) { + if (!queue.empty()) { + queueSelIdx = std::min(static_cast(queue.size()) - 1, + queueSelIdx + 5); + } + } else if (sym == XK_Return || sym == XK_KP_Enter) { + if (!queue.empty()) { + playTrackAtIndex(queueSelIdx); + activeTab = TabId::Player; + } + } else if (dch == '5') { + togglePlayPause(); + } else if (dch == '*' && !queue.empty()) { + installTrackAsRingtone(queue[queueSelIdx]); + } else if (dch == '0') { + populateDefaultQueue(); + setBanner("Rescanned Music (" + std::to_string(queue.size()) + + " tracks)"); + } + render(); + } else if (activeTab == TabId::Browser) { + if (sym == XK_Up || sym == XK_KP_Up) { + if (!browserItems.empty()) { + browserSelIdx = + (browserSelIdx + static_cast(browserItems.size()) - 1) % + static_cast(browserItems.size()); + } + } else if (sym == XK_Down || sym == XK_KP_Down) { + if (!browserItems.empty()) { + browserSelIdx = + (browserSelIdx + 1) % static_cast(browserItems.size()); + } + } else if (sym == XK_Left || sym == XK_KP_Left || + sym == XK_BackSpace || sym == XK_Delete) { + if (browserDir != "/") { + std::string prevBase = browserDir; + size_t sl = prevBase.rfind('/'); + std::string prevName = + (sl != std::string::npos) ? prevBase.substr(sl + 1) : ""; + std::string upDir = (sl == 0 || sl == std::string::npos) + ? "/" + : prevBase.substr(0, sl); + loadBrowserDirectory(upDir, prevName); + } + } else if (sym == XK_Right || sym == XK_KP_Right || + sym == XK_Return || sym == XK_KP_Enter) { + handleBrowserSelect(false); + } else if (dch == '5') { + togglePlayPause(); + } + render(); + } + } + } + + if (mpvPid > 0 && ipcFd < 0) { + tryConnectIpc(); + } + if (drainIpcSocket() && visible) { + render(); + } + + int timeoutMs = -1; + int64_t now = nowMs(); + if (mpvPid > 0 && ipcFd < 0) { + timeoutMs = 35; + } + if (visible) { + if (softkeyPending != 0) { + int64_t rem = softkeyDeadlineMs - now; + if (rem <= 0) { + int pend = softkeyPending; + softkeyPending = 0; + if (querySoftkeysDown(dpy) == 3) { + writeSoftkeySuppress(now + 400); + hideWindow(); + sendPluginCommand("xdrawer", "TOGGLE"); + } else if (pend == 1) { + if (activeTab == TabId::Player) { + activeTab = TabId::Queue; + } else if (activeTab == TabId::Queue) { + if (!queue.empty()) { + playTrackAtIndex(queueSelIdx); + activeTab = TabId::Player; + } + } else if (activeTab == TabId::Browser) { + handleBrowserSelect(true); + } + render(); + } else if (pend == 2) { + hideWindow(); + } + } else { + timeoutMs = (timeoutMs < 0) ? static_cast(rem) + : std::min(timeoutMs, static_cast(rem)); + } + } + if (!statusBanner.empty()) { + int64_t bRem = statusBannerUntilMs - now; + if (bRem <= 0) { + statusBanner.clear(); + render(); + } else if (timeoutMs < 0 || bRem < timeoutMs) { + timeoutMs = static_cast(bRem); + } + } + if (isPlaying && !isPaused && (timeoutMs < 0 || timeoutMs > 250)) { + timeoutMs = 250; + } + } + + struct pollfd pfds[3]; + int nfds = 0; + pfds[nfds++] = {xfd, POLLIN, 0}; + int fifoIdx = -1; + if (fifoFd >= 0) { + fifoIdx = nfds; + pfds[nfds++] = {fifoFd, POLLIN, 0}; + } + int ipcIdx = -1; + if (ipcFd >= 0) { + ipcIdx = nfds; + pfds[nfds++] = {ipcFd, POLLIN, 0}; + } + + int pr = ::poll(pfds, nfds, timeoutMs); + if (pr > 0) { + if (ipcIdx >= 0 && (pfds[ipcIdx].revents & (POLLIN | POLLHUP | POLLERR))) { + if (drainIpcSocket() && visible) { + render(); + } + } + if (fifoIdx >= 0 && (pfds[fifoIdx].revents & POLLIN)) { + for (const auto& cmd : readFifoLines(fifoFd)) { + if (cmd == "CLOSE") { + hideWindow(); + } else if (cmd == "RELOAD_THEME") { + pt.reload(); + if (visible) render(); + } else if (cmd == "PAUSE") { + pausePlayback(); + if (visible) render(); + } else if (cmd == "STOP") { + stopPlayback(); + if (visible) render(); + } else if (cmd == "TOGGLE") { + if (visible) hideWindow(); + else showWindow(); + } else if (cmd == "OPEN") { + showWindow(); + } else if (cmd.rfind("OPEN ", 0) == 0) { + showWindow(trimStr(cmd.substr(5))); + } + } + } + } + } + + hideWindow(); + stopMpvDaemon(); + if (fifoFd >= 0) ::close(fifoFd); + ::unlink(daemonPidPath.c_str()); + if (xftDraw) XftDrawDestroy(xftDraw); + XFreeGC(dpy, gc); + XFreePixmap(dpy, pix); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} + +int pluginInit(const DwmHostApi* host) { + if (host && host->spawn_plugin) { + host->spawn_plugin("music", "--preload"); + } + return 0; +} + +void pluginCleanup(void) {} + +} // namespace + +extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { + DWM_PLUGIN_API_VERSION, + "music", + "Bananarchy Music & Tracker Player (240x320)", + pluginInit, + pluginCleanup, + nullptr, + nullptr, + nullptr, + runMusicApp +}; diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index f5fa076..e3447d8 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -1156,6 +1156,7 @@ inline void closeOtherPlugins(const char* selfPluginName) { {"xnetworks.pid", "networks"}, {"xringtones.pid", "ringtones"}, {"xwallpapers.pid", "wallpapers"}, + {"xmusic.pid", "music"}, }; for (const auto& p : kAllPlugins) { if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) { @@ -1190,6 +1191,7 @@ inline bool isAnotherPluginActive(const char* selfPluginName) { {"xnetworks.pid", "networks"}, {"xringtones.pid", "ringtones"}, {"xwallpapers.pid", "wallpapers"}, + {"xmusic.pid", "music"}, }; for (const auto& p : kAllPlugins) { if (selfPluginName && std::strcmp(p.name, selfPluginName) == 0) { @@ -1233,6 +1235,7 @@ inline void broadcastThemeReload(const char* selfPluginName = nullptr) { "networks", "ringtones", "wallpapers", + "music", }; for (const char* name : kPlugins) { if (selfPluginName && std::strcmp(name, selfPluginName) == 0) { diff --git a/src/dwm-plugins/ringtones_plugin.cpp b/src/dwm-plugins/ringtones_plugin.cpp index e479ad8..13a25c9 100644 --- a/src/dwm-plugins/ringtones_plugin.cpp +++ b/src/dwm-plugins/ringtones_plugin.cpp @@ -43,6 +43,7 @@ struct RingtoneEntry { struct RingSettings { int ringerMode = 0; // 0 = Ring + Vibrate, 1 = Ring Only, 2 = Vibrate Only, 3 = Silent + int ringVol = 100; int ringTimeSec = 20; int ringLoops = 10; int vibrateStrength = 1; @@ -198,6 +199,8 @@ RingSettings loadRingSettings() { s.ringLoops = val; } else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) { s.vibrateStrength = val; + } else if (k == "SXMO_RINGVOL" && val >= 0 && val <= 100) { + s.ringVol = (val / 5) * 5; } } return s; @@ -228,6 +231,7 @@ void saveRingSettings(const RingSettings& s) { ofs << "SXMO_RINGTIME=" << s.ringTimeSec << "\n"; ofs << "SXMO_RINGNUMBER=" << s.ringLoops << "\n"; ofs << "SXMO_VIBRATE_STRENGTH=" << s.vibrateStrength << "\n"; + ofs << "SXMO_RINGVOL=" << std::clamp(s.ringVol, 0, 100) << "\n"; } runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1"); } @@ -372,6 +376,16 @@ int runRingtonesApp(int argc, char** argv) { return; } stopPreview(); + settings = loadRingSettings(); + int rvol = std::clamp(settings.ringVol, 0, 100); + int rx3 = (rvol <= 0) ? 0 : (60 + rvol * 34 / 100); + int mpvVol = rvol * 130 / 100; + std::string volArg = "--volume=" + std::to_string(mpvVol); + runAsyncCmd("amixer -c 0 cset \"name='SPK DAC Switch'\" 1 >/dev/null 2>&1; " + "amixer -c 0 cset \"name='RX3 Digital Volume'\" " + + std::to_string(rx3) + " >/dev/null 2>&1; " + "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1; " + "wpctl set-volume @DEFAULT_AUDIO_SINK@ 100% >/dev/null 2>&1"); pid_t pid = ::fork(); if (pid == 0) { int devNull = ::open("/dev/null", O_RDWR); @@ -382,7 +396,8 @@ int runRingtonesApp(int argc, char** argv) { if (devNull > 2) ::close(devNull); } ::execlp("mpv", "mpv", "--no-resume-playback", "--quiet", - "--no-video", "--loop=2", path.c_str(), nullptr); + "--no-video", "--volume-max=150", volArg.c_str(), + "--loop=2", path.c_str(), nullptr); ::_exit(127); } else if (pid > 0) { previewPid = pid; @@ -532,9 +547,11 @@ int runRingtonesApp(int argc, char** argv) { "Vibrate Only", "Silent", }; - std::string rows[5][2] = { + std::string rows[6][2] = { {"Ringer Mode", "< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"}, + {"Ring Volume", + "< " + std::to_string(settings.ringVol) + "% >"}, {"Ring Duration", "< " + std::to_string(settings.ringTimeSec) + " sec >"}, {"Ring Loop Count", @@ -544,18 +561,18 @@ int runRingtonesApp(int argc, char** argv) { {"Test Vibration", "Press OK to vibrate"}, }; - for (int i = 0; i < 5; ++i) { - int ry = 32 + i * 42; + for (int i = 0; i < 6; ++i) { + int ry = 30 + i * 35; bool isSel = (settingsRow == i); XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven); - XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 38); + XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 33); if (isSel) { XSetForeground(dpy, gc, pt.spec.accent); - XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 37); + XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 32); } - drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 2, 17, + drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 1, 15, rows[i][0], kWinWidth - 20); - drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 18, 19, + drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 15, 17, rows[i][1], kWinWidth - 20); } drawSoftkeyBar("Change", "Select", "Close"); @@ -629,6 +646,10 @@ int runRingtonesApp(int argc, char** argv) { saveRingSettings(settings); setBanner("Ringer mode updated"); } else if (settingsRow == 1) { + settings.ringVol = std::clamp(settings.ringVol + delta * 5, 0, 100); + saveRingSettings(settings); + setBanner("Ring volume: " + std::to_string(settings.ringVol) + "%"); + } else if (settingsRow == 2) { int curIdx = 2; for (int i = 0; i < kNumRingTimeSteps; ++i) { if (kRingTimeSteps[i] == settings.ringTimeSec) { @@ -639,7 +660,7 @@ int runRingtonesApp(int argc, char** argv) { curIdx = (curIdx + kNumRingTimeSteps + delta) % kNumRingTimeSteps; settings.ringTimeSec = kRingTimeSteps[curIdx]; saveRingSettings(settings); - } else if (settingsRow == 2) { + } else if (settingsRow == 3) { int curIdx = 2; for (int i = 0; i < kNumRingLoopSteps; ++i) { if (kRingLoopSteps[i] == settings.ringLoops) { @@ -650,14 +671,14 @@ int runRingtonesApp(int argc, char** argv) { curIdx = (curIdx + kNumRingLoopSteps + delta) % kNumRingLoopSteps; settings.ringLoops = kRingLoopSteps[curIdx]; saveRingSettings(settings); - } else if (settingsRow == 3) { + } else if (settingsRow == 4) { settings.vibrateStrength = ((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1; saveRingSettings(settings); - } else if (settingsRow == 4) { + } else if (settingsRow == 5) { int mag = (settings.vibrateStrength == 1) - ? 20000 - : (settings.vibrateStrength == 3 ? 65535 : 40000); + ? 8000 + : (settings.vibrateStrength == 3 ? 65535 : 32000); runAsyncCmd( "for d in /dev/input/by-path/platform-vibrator-event " "/dev/input/by-path/*vibrator*-event; do " @@ -744,7 +765,7 @@ int runRingtonesApp(int argc, char** argv) { } if (sym == XK_Left || sym == XK_KP_Left) { - if (activeTab == TabId::Settings && settingsRow < 4) { + if (activeTab == TabId::Settings && settingsRow < 5) { adjustSettingRow(-1); } else { activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings @@ -752,7 +773,7 @@ int runRingtonesApp(int argc, char** argv) { } render(); } else if (sym == XK_Right || sym == XK_KP_Right) { - if (activeTab == TabId::Settings && settingsRow < 4) { + if (activeTab == TabId::Settings && settingsRow < 5) { adjustSettingRow(1); } else { activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings @@ -764,14 +785,14 @@ int runRingtonesApp(int argc, char** argv) { selIdx = (selIdx + static_cast(tones.size()) - 1) % static_cast(tones.size()); } else if (activeTab == TabId::Settings) { - settingsRow = (settingsRow + 4) % 5; + settingsRow = (settingsRow + 5) % 6; } render(); } else if (sym == XK_Down || sym == XK_KP_Down) { if (activeTab == TabId::Ringtones && !tones.empty()) { selIdx = (selIdx + 1) % static_cast(tones.size()); } else if (activeTab == TabId::Settings) { - settingsRow = (settingsRow + 1) % 5; + settingsRow = (settingsRow + 1) % 6; } render(); } else if (sym == XK_Return || sym == XK_KP_Enter) { diff --git a/src/dwm-plugins/sxmo_vibrate.c b/src/dwm-plugins/sxmo_vibrate.c new file mode 100644 index 0000000..910a596 --- /dev/null +++ b/src/dwm-plugins/sxmo_vibrate.c @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int read_configured_level(void) { + char path[512]; + const char* cfg = getenv("XDG_CONFIG_HOME"); + const char* home = getenv("HOME"); + if (cfg && *cfg) { + snprintf(path, sizeof(path), "%s/sxmo/ringsettings", cfg); + } else if (home && *home) { + snprintf(path, sizeof(path), "%s/.config/sxmo/ringsettings", home); + } else { + snprintf(path, sizeof(path), "/home/user/.config/sxmo/ringsettings"); + } + + FILE* fp = fopen(path, "r"); + if (!fp) return 0; + + int level = 0; + char line[256]; + while (fgets(line, sizeof(line), fp)) { + char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (strncmp(p, "SXMO_VIBRATE_STRENGTH=", 22) == 0) { + int v = atoi(p + 22); + if (v >= 1 && v <= 3) level = v; + } + } + fclose(fp); + return level; +} + +static int resolve_magnitude(int raw_arg, int has_arg) { + int val = raw_arg; + if (!has_arg || (val == 1 && !getenv("SXMO_VIBRATE_STRENGTH"))) { + int cfg = read_configured_level(); + if (cfg >= 1 && cfg <= 3) val = cfg; + } + if (val <= 1) return 8000; + if (val == 2) return 32000; + if (val == 3) return 65535; + if (val < 256) return val * 257; + if (val > 65535) return 65535; + return val; +} + +static int open_vibrator_device(void) { + const char* env_dev = getenv("SXMO_VIBRATE_DEV"); + if (env_dev && *env_dev) { + int fd = open(env_dev, O_RDWR | O_CLOEXEC); + if (fd >= 0) return fd; + } + + int fd = open("/dev/input/by-path/platform-vibrator-event", O_RDWR | O_CLOEXEC); + if (fd >= 0) return fd; + + glob_t g; + memset(&g, 0, sizeof(g)); + if (glob("/dev/input/by-path/*vibrator*-event", 0, NULL, &g) == 0) { + for (size_t i = 0; i < g.gl_pathc; ++i) { + fd = open(g.gl_pathv[i], O_RDWR | O_CLOEXEC); + if (fd >= 0) { + globfree(&g); + return fd; + } + } + globfree(&g); + } + return -1; +} + +int main(int argc, char** argv) { + if (argc < 2 || argc > 3) { + fprintf(stderr, "Usage: sxmo_vibrate duration_ms [strength_number]\n"); + return 1; + } + + char* endp = NULL; + errno = 0; + long duration_ms = strtol(argv[1], &endp, 10); + if (errno != 0 || !endp || *endp != '\0' || duration_ms <= 0) { + fprintf(stderr, "sxmo_vibrate: invalid duration\n"); + return 1; + } + + int raw_strength = 2; + int has_strength = 0; + if (argc == 3) { + endp = NULL; + errno = 0; + long s = strtol(argv[2], &endp, 10); + if (errno == 0 && endp && *endp == '\0' && s >= 1 && s <= 65535) { + raw_strength = (int)s; + has_strength = 1; + } + } + + int magnitude = resolve_magnitude(raw_strength, has_strength); + + int fd = open_vibrator_device(); + if (fd < 0) { + fprintf(stderr, "sxmo_vibrate: cannot open vibrator device: %s\n", + strerror(errno)); + return 1; + } + + struct ff_effect effect; + memset(&effect, 0, sizeof(effect)); + effect.type = FF_RUMBLE; + effect.id = -1; + effect.u.rumble.strong_magnitude = (unsigned short)magnitude; + effect.u.rumble.weak_magnitude = (unsigned short)magnitude; + + if (ioctl(fd, EVIOCSFF, &effect) < 0) { + close(fd); + return 1; + } + + struct input_event play; + memset(&play, 0, sizeof(play)); + play.type = EV_FF; + play.code = (unsigned short)effect.id; + play.value = 3; + + if (write(fd, &play, sizeof(play)) < 0) { + ioctl(fd, EVIOCRMFF, effect.id); + close(fd); + return 1; + } + + struct timespec req; + req.tv_sec = duration_ms / 1000; + req.tv_nsec = (duration_ms % 1000) * 1000000L; + while (nanosleep(&req, &req) == -1 && errno == EINTR) { + } + + play.value = 0; + (void)write(fd, &play, sizeof(play)); + (void)ioctl(fd, EVIOCRMFF, effect.id); + close(fd); + return 0; +} diff --git a/src/sxmo-dmenu/dmenu.c b/src/sxmo-dmenu/dmenu.c index b1cd33b..6777445 100644 --- a/src/sxmo-dmenu/dmenu.c +++ b/src/sxmo-dmenu/dmenu.c @@ -2275,6 +2275,9 @@ open_builtin_menu(const char *menu_name, int keep_idx) snprintf(label, sizeof(label), "%s Camera", ICON_CAM); add_menu_item(label, "@plugin:camera", 0); + snprintf(label, sizeof(label), "%s Music", ICON_MUS); + add_menu_item(label, "@plugin:music", 0); + snprintf(label, sizeof(label), "%s Themes", ICON_THM); add_menu_item(label, "@plugin:themes", 0); @@ -2337,6 +2340,8 @@ open_builtin_menu(const char *menu_name, int keep_idx) add_menu_item(label, "@plugin:wallpapers", 0); snprintf(label, sizeof(label), "%s Ringtones", ICON_BEL); add_menu_item(label, "@plugin:ringtones", 0); + snprintf(label, sizeof(label), "%s Music", ICON_MUS); + add_menu_item(label, "@plugin:music", 0); snprintf(label, sizeof(label), "%s System Menu", ICON_MNU); add_menu_item(label, "@menu:sys", 1); snprintf(label, sizeof(label), "%s Close Menu", ICON_CLS); @@ -2401,6 +2406,9 @@ open_builtin_menu(const char *menu_name, int keep_idx) rdir = "/run/user/10000"; char pbuf[256]; + snprintf(label, sizeof(label), "%s Music Player", ICON_MUS); + add_menu_item(label, "@plugin:music", 0); + snprintf(label, sizeof(label), "%s Volume & Controls (XDrawer)", ICON_MUS); add_menu_item(label, "@plugin:xdrawer", 0); diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp index 5d7ee0c..a9d8c7f 100644 --- a/src/xdrawer/xdrawer.cpp +++ b/src/xdrawer/xdrawer.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -42,7 +43,7 @@ constexpr int kWinX = 0; constexpr int kWinY = kTopBarHeight; constexpr int kWinWidth = kScreenWidth; constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar -constexpr int kRowCount = 8; +constexpr int kRowCount = 9; // Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font") constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker @@ -239,12 +240,13 @@ class AlsaControl { enum class ControlId { SpeakerVol = 0, EarpieceVol = 1, - Flashlight = 2, - SoundMute = 3, - MicMute = 4, - RingerMode = 5, - ScreenBright = 6, - KbdBright = 7, + RingVol = 2, + RingerMode = 3, + Flashlight = 4, + SoundMute = 5, + MicMute = 6, + ScreenBright = 7, + KbdBright = 8, }; struct ControlItem { @@ -282,31 +284,55 @@ class PhoneController { void refreshAll() { items_.clear(); - // 1. Speaker volume (0..100%, mapped to ALSA RX3 Digital Volume 0..84) - long rx3 = alsa_.getInteger("RX3 Digital Volume", 84); - int spkPct = std::clamp(static_cast(std::lround((rx3 * 100.0) / 84.0)), 0, 100); + // 1. Speaker volume (0..100%, mapped to ALSA RX3 Digital Volume 60..92 + PipeWire 0..100%) + int spkPct = readIntFile(getSxmoCfgDir() + "/speaker_vol", -1); + if (spkPct < 0 || spkPct > 100) { + long rx3 = alsa_.getInteger("RX3 Digital Volume", 92); + if (rx3 <= 0) { + spkPct = 0; + } else if (rx3 >= 84) { + spkPct = 100; + } else { + spkPct = std::clamp(static_cast(std::lround(((rx3 - 60) * 100.0) / 32.0)), 5, 100); + } + } spkPct = (spkPct / 5) * 5; items_.push_back({ControlId::SpeakerVol, kIconSpeaker, kIconSoundOff, "Speaker", true, 0, 100, 5, spkPct}); - // 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 0..84) + // 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 58..90) long rx1 = alsa_.getInteger("RX1 Digital Volume", 84); - int earPct = std::clamp(static_cast(std::lround((rx1 * 100.0) / 84.0)), 0, 100); + int earPct = 100; + if (rx1 <= 0) { + earPct = 0; + } else if (rx1 < 84) { + earPct = std::clamp(static_cast(std::lround((rx1 * 100.0) / 84.0)), 5, 100); + } earPct = (earPct / 5) * 5; items_.push_back({ControlId::EarpieceVol, kIconEarpiece, kIconEarpiece, "Earpiece", true, 0, 100, 5, earPct}); - // 3. Flashlight on/off (toggle) + // 3. Ringtone volume (0..100%, stored as SXMO_RINGVOL in ~/.config/sxmo/ringsettings) + int ringPct = readRingVolume(); + items_.push_back({ControlId::RingVol, kIconBell, kIconBellSlash, + "Ring Vol", true, 0, 100, 5, ringPct}); + + // 4. Ringer Mode (0=Ring+Vib, 1=Vibrate, 2=Silent, 3=DND) + int ringerVal = readRingerMode(); + items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash, + "Ringer", false, 0, 3, 1, ringerVal}); + + // 5. Flashlight on/off (toggle) int torchCur = readIntFile(torchDir_ + "/brightness", 0); items_.push_back({ControlId::Flashlight, kIconFlashlight, kIconFlashlight, "Flashlight", false, 0, 1, 1, (torchCur > 0) ? 1 : 0}); - // 4. Sound on/off (toggle) + // 6. Sound on/off (toggle) bool spkOn = alsa_.getBoolean("SPK DAC Switch", true); items_.push_back({ControlId::SoundMute, kIconSoundOn, kIconSoundOff, "Sound", false, 0, 1, 1, spkOn ? 1 : 0}); - // 5. Microphone on/off (toggle) + // 7. Microphone on/off (toggle) unsigned int dec1 = alsa_.getEnum("DEC1 MUX", 2); if (dec1 > 0) { savedMicMux_ = dec1; @@ -317,18 +343,13 @@ class PhoneController { items_.push_back({ControlId::MicMute, kIconMicOn, kIconMicOff, "Microphone", false, 0, 1, 1, (dec1 > 0) ? 1 : 0}); - // 6. Ringer Mode (0=Ring+Vib, 1=Vibrate, 2=Silent, 3=DND) - int ringerVal = readRingerMode(); - items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash, - "Ringer", false, 0, 3, 1, ringerVal}); - - // 7. Screen brightness (slider 1..screenMax_) + // 8. Screen brightness (slider 1..screenMax_) int scrCur = std::clamp(readIntFile(backlightDir_ + "/brightness", screenMax_), 1, screenMax_); int scrStep = std::max(1, screenMax_ / 14); items_.push_back({ControlId::ScreenBright, kIconBrightness, kIconBrightness, "Screen", true, 1, screenMax_, scrStep, scrCur}); - // 7. Keyboard brightness (slider if max_brightness > 1, toggle otherwise) + // 9. Keyboard brightness (slider if max_brightness > 1, toggle otherwise) int kbdCur = std::clamp(readIntFile(kbdDir_ + "/brightness", 0), 0, kbdMax_); if (kbdSupportsSlider_) { int kbdStep = std::max(1, kbdMax_ / 10); @@ -395,25 +416,81 @@ class PhoneController { return 0; // Ring + Vibrate } + static int readRingVolume() { + std::ifstream ifs(getSxmoCfgDir() + "/ringsettings"); + std::string line; + while (std::getline(ifs, line)) { + auto eq = line.find('='); + if (eq == std::string::npos) continue; + std::string k = line.substr(0, eq); + std::string v = line.substr(eq + 1); + if (k == "SXMO_RINGVOL") { + int val = std::clamp(std::atoi(v.c_str()), 0, 100); + return (val / 5) * 5; + } + } + return 100; + } + + static void writeRingVolume(int ringVol) { + std::string cfg = getSxmoCfgDir(); + ::mkdir(cfg.c_str(), 0755); + std::string path = cfg + "/ringsettings"; + int ringTime = 20; + int ringLoops = 10; + int vibStrength = 2; + { + std::ifstream ifs(path); + std::string line; + while (std::getline(ifs, line)) { + auto eq = line.find('='); + if (eq == std::string::npos) continue; + std::string k = line.substr(0, eq); + int val = std::atoi(line.substr(eq + 1).c_str()); + if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) ringTime = val; + else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) ringLoops = val; + else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) vibStrength = val; + } + } + std::ofstream ofs(path, std::ios::trunc); + if (ofs) { + ofs << "SXMO_RINGTIME=" << ringTime << "\n"; + ofs << "SXMO_RINGNUMBER=" << ringLoops << "\n"; + ofs << "SXMO_VIBRATE_STRENGTH=" << vibStrength << "\n"; + ofs << "SXMO_RINGVOL=" << std::clamp(ringVol, 0, 100) << "\n"; + } + } + void apply(ControlItem& it) { switch (it.id) { case ControlId::SpeakerVol: { - long hw = std::lround((it.value * 84.0) / 100.0); + std::string cfg = getSxmoCfgDir(); + ::mkdir(cfg.c_str(), 0755); + writeIntFile(cfg + "/speaker_vol", it.value); + long hw = (it.value <= 0) ? 0 : (60 + std::lround((it.value * 32.0) / 100.0)); + if (it.value > 0) { + alsa_.setBoolean("SPK DAC Switch", true); + } alsa_.setInteger("RX3 Digital Volume", hw); - char cmd[128]; + char cmd[160]; std::snprintf( cmd, sizeof(cmd), + "wpctl set-mute @DEFAULT_AUDIO_SINK@ %d >/dev/null 2>&1; " "wpctl set-volume @DEFAULT_AUDIO_SINK@ %d%% >/dev/null 2>&1; " - "sxmo_hook_statusbar.sh audio >/dev/null 2>&1", - it.value); + "sxmo_hook_statusbar.sh volume >/dev/null 2>&1", + (it.value <= 0) ? 1 : 0, it.value); runAsyncCmd(cmd); break; } case ControlId::EarpieceVol: { - long hw = std::lround((it.value * 84.0) / 100.0); + long hw = (it.value <= 0) ? 0 : (58 + std::lround((it.value * 32.0) / 100.0)); alsa_.setInteger("RX1 Digital Volume", hw); break; } + case ControlId::RingVol: { + writeRingVolume(it.value); + break; + } case ControlId::Flashlight: { int v = it.value ? torchMax_ : 0; if (!writeIntFile(torchDir_ + "/brightness", v)) { @@ -994,7 +1071,9 @@ static int xdrawer_run_main(int argc, char** argv) { case XK_4: case XK_5: case XK_6: - case XK_7: { + case XK_7: + case XK_8: + case XK_9: { size_t idx = static_cast(sym - XK_1); if (idx < count) { selected = idx;