@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +556 -0
- package/INSTALLATION.md +310 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/client.js +3 -0
- package/components.json +20 -0
- package/cordis.patch.yml +3 -0
- package/docs/README.en.md +196 -0
- package/docs/screenshots/en/overview.png +0 -0
- package/docs/screenshots/en/question.png +0 -0
- package/docs/screenshots/zh/overview.png +0 -0
- package/docs/screenshots/zh/question.png +0 -0
- package/icon.svg +24 -0
- package/index.js +2 -0
- package/lib/account.js +1 -0
- package/lib/app-mapping.js +1 -0
- package/lib/i18n.js +1 -0
- package/lib/media-enrich.js +1 -0
- package/lib/notifications.js +1 -0
- package/lib/performance.js +1 -0
- package/lib/platform/index.js +1 -0
- package/lib/probe-adapter.js +1 -0
- package/lib/probe-daemon.js +1 -0
- package/lib/probe.js +1 -0
- package/lib/qr.js +1 -0
- package/lib/runtime.js +1 -0
- package/lib/service.js +1 -0
- package/lib/sessions.js +1 -0
- package/lib/settings.js +1 -0
- package/lib/types.js +1 -0
- package/lib/util.js +1 -0
- package/lib/weather.js +1 -0
- package/lib/ws.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +118 -4
- package/scripts/browser-shot.mjs +217 -0
- package/scripts/build-dsh.mjs +228 -0
- package/scripts/build-rust.mjs +319 -0
- package/scripts/build-web.mjs +17 -0
- package/scripts/check-npm-package.mjs +322 -0
- package/scripts/mock-preview.py +574 -0
- package/scripts/screenshot-compose.py +89 -0
- package/scripts/screenshot-device.sh +75 -0
- package/src/dsh/client/api.ts +68 -0
- package/src/dsh/client/i18n.ts +573 -0
- package/src/dsh/client/icons.ts +56 -0
- package/src/dsh/client/index.ts +95 -0
- package/src/dsh/client/logic.ts +310 -0
- package/src/dsh/client/panel.ts +483 -0
- package/src/dsh/client/sections/apps.ts +40 -0
- package/src/dsh/client/sections/balance.ts +38 -0
- package/src/dsh/client/sections/connection.ts +97 -0
- package/src/dsh/client/sections/notifications.ts +43 -0
- package/src/dsh/client/sections/pomodoro.ts +184 -0
- package/src/dsh/client/sections/preferences.ts +138 -0
- package/src/dsh/client/sections/system.ts +120 -0
- package/src/dsh/client/sections/weather.ts +276 -0
- package/src/dsh/client/styles.ts +1119 -0
- package/src/dsh/client/types.ts +366 -0
- package/src/dsh/client/ui.ts +142 -0
- package/src/dsh/index.ts +108 -0
- package/src/dsh/lib/account.ts +520 -0
- package/src/dsh/lib/app-mapping.ts +366 -0
- package/src/dsh/lib/i18n.ts +82 -0
- package/src/dsh/lib/media-enrich.ts +202 -0
- package/src/dsh/lib/notifications.ts +322 -0
- package/src/dsh/lib/performance.ts +279 -0
- package/src/dsh/lib/platform/index.ts +43 -0
- package/src/dsh/lib/probe-adapter.ts +275 -0
- package/src/dsh/lib/probe-daemon.ts +359 -0
- package/src/dsh/lib/probe.ts +395 -0
- package/src/dsh/lib/qr.ts +42 -0
- package/src/dsh/lib/runtime.ts +126 -0
- package/src/dsh/lib/service.ts +1645 -0
- package/src/dsh/lib/sessions.ts +907 -0
- package/src/dsh/lib/settings.ts +468 -0
- package/src/dsh/lib/types.ts +231 -0
- package/src/dsh/lib/util.ts +115 -0
- package/src/dsh/lib/weather.ts +532 -0
- package/src/dsh/lib/ws.ts +328 -0
- package/src/dsh/types/qrcode-generator.d.ts +16 -0
- package/src/rust/Cargo.lock +1431 -0
- package/src/rust/Cargo.toml +68 -0
- package/src/rust/src/cli.rs +233 -0
- package/src/rust/src/daemon.rs +140 -0
- package/src/rust/src/location/linux.rs +118 -0
- package/src/rust/src/location/macos.rs +26 -0
- package/src/rust/src/location/mod.rs +170 -0
- package/src/rust/src/location/windows.rs +205 -0
- package/src/rust/src/main.rs +42 -0
- package/src/rust/src/media/catalog.rs +454 -0
- package/src/rust/src/media/linux.rs +350 -0
- package/src/rust/src/media/macos/accessibility.rs +59 -0
- package/src/rust/src/media/macos.rs +366 -0
- package/src/rust/src/media/mod.rs +298 -0
- package/src/rust/src/media/vendor/apple_music.rs +90 -0
- package/src/rust/src/media/vendor/common.rs +186 -0
- package/src/rust/src/media/vendor/kugou.rs +52 -0
- package/src/rust/src/media/vendor/kuwo.rs +49 -0
- package/src/rust/src/media/vendor/mod.rs +75 -0
- package/src/rust/src/media/vendor/netease.rs +1715 -0
- package/src/rust/src/media/vendor/qishui.rs +36 -0
- package/src/rust/src/media/vendor/qqmusic.rs +33 -0
- package/src/rust/src/media/vendor/spotify.rs +46 -0
- package/src/rust/src/media/vendor/wesing.rs +39 -0
- package/src/rust/src/media/vendor/youtube_music.rs +49 -0
- package/src/rust/src/media/windows/accessibility.rs +85 -0
- package/src/rust/src/media/windows/audio.rs +116 -0
- package/src/rust/src/media/windows/private.rs +291 -0
- package/src/rust/src/media/windows.rs +775 -0
- package/src/rust/src/notifications/linux.rs +276 -0
- package/src/rust/src/notifications/macos.rs +382 -0
- package/src/rust/src/notifications/mod.rs +244 -0
- package/src/rust/src/notifications/windows.rs +565 -0
- package/src/rust/src/perf/common.rs +109 -0
- package/src/rust/src/perf/linux.rs +282 -0
- package/src/rust/src/perf/macos.rs +28 -0
- package/src/rust/src/perf/mod.rs +157 -0
- package/src/rust/src/perf/windows.rs +981 -0
- package/src/rust/src/util.rs +360 -0
- package/src/rust/src/volume/linux.rs +195 -0
- package/src/rust/src/volume/macos.rs +54 -0
- package/src/rust/src/volume/mod.rs +116 -0
- package/src/rust/src/volume/windows.rs +75 -0
- package/src/rust/tests/cli.rs +79 -0
- package/src/rust/tests/daemon.rs +107 -0
- package/src/svelte/App.svelte +385 -0
- package/src/svelte/index.html +18 -0
- package/src/svelte/lib/billing.svelte.ts +228 -0
- package/src/svelte/lib/components/BalanceCard.svelte +173 -0
- package/src/svelte/lib/components/ClockCard.svelte +183 -0
- package/src/svelte/lib/components/CountdownCard.svelte +261 -0
- package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
- package/src/svelte/lib/components/EmptyState.svelte +34 -0
- package/src/svelte/lib/components/GpuCard.svelte +245 -0
- package/src/svelte/lib/components/HomePage.svelte +34 -0
- package/src/svelte/lib/components/MusicControls.svelte +256 -0
- package/src/svelte/lib/components/MusicCover.svelte +88 -0
- package/src/svelte/lib/components/MusicPage.svelte +116 -0
- package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
- package/src/svelte/lib/components/Panel.svelte +22 -0
- package/src/svelte/lib/components/PerformancePage.svelte +55 -0
- package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
- package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
- package/src/svelte/lib/components/RadialGauge.svelte +99 -0
- package/src/svelte/lib/components/Rail.svelte +217 -0
- package/src/svelte/lib/components/SectionHeader.svelte +40 -0
- package/src/svelte/lib/components/SeekBar.svelte +421 -0
- package/src/svelte/lib/components/SessionList.svelte +364 -0
- package/src/svelte/lib/components/SessionStats.svelte +98 -0
- package/src/svelte/lib/components/SessionsPage.svelte +61 -0
- package/src/svelte/lib/components/SpendCard.svelte +105 -0
- package/src/svelte/lib/components/ThermalCard.svelte +249 -0
- package/src/svelte/lib/components/Toast.svelte +32 -0
- package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
- package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
- package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
- package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
- package/src/svelte/lib/components/ui/alert/index.ts +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
- package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
- package/src/svelte/lib/components/ui/badge/index.ts +2 -0
- package/src/svelte/lib/components/ui/button/button.svelte +82 -0
- package/src/svelte/lib/components/ui/button/index.ts +17 -0
- package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card.svelte +22 -0
- package/src/svelte/lib/components/ui/card/index.ts +25 -0
- package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
- package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
- package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/index.ts +22 -0
- package/src/svelte/lib/components/ui/input/index.ts +7 -0
- package/src/svelte/lib/components/ui/input/input.svelte +48 -0
- package/src/svelte/lib/components/ui/progress/index.ts +7 -0
- package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
- package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
- package/src/svelte/lib/components/ui/separator/index.ts +7 -0
- package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
- package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
- package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
- package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
- package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/src/svelte/lib/components/ui/slider/index.ts +7 -0
- package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
- package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/svelte/lib/countdown.svelte.ts +252 -0
- package/src/svelte/lib/format.ts +199 -0
- package/src/svelte/lib/i18n.svelte.ts +583 -0
- package/src/svelte/lib/media.ts +32 -0
- package/src/svelte/lib/pages.ts +18 -0
- package/src/svelte/lib/pomodoro.svelte.ts +223 -0
- package/src/svelte/lib/session-tree.ts +80 -0
- package/src/svelte/lib/state.svelte.ts +359 -0
- package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
- package/src/svelte/lib/types.ts +306 -0
- package/src/svelte/lib/utils.ts +8 -0
- package/src/svelte/lib/weather.ts +84 -0
- package/src/svelte/lib/ws.ts +104 -0
- package/src/svelte/main.ts +13 -0
- package/src/svelte/styles/global.css +315 -0
- package/src/svelte/vite-env.d.ts +12 -0
- package/static/fonts/LICENSE.txt +287 -0
- package/static/icon.svg +24 -0
- package/static/manifest.webmanifest +18 -0
- package/svelte.config.js +5 -0
- package/tsconfig.dsh-client.json +18 -0
- package/tsconfig.dsh.json +21 -0
- package/tsconfig.json +20 -0
- package/vite.config.js +31 -0
- package/web/assets/index-DLdSfnJ3.css +1 -0
- package/web/assets/index-YNE8X0Uy.js +6 -0
- package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
- package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
- package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
- package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
- package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
- package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
- package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
- package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
- package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
- package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
- package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
- package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
- package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
- package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
- package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
- package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
- package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
- package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
- package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
- package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
- package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
- package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
- package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
- package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
- package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
- package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
- package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
- package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
- package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
- package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
- package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
- package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
- package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
- package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
- package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
- package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
- package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
- package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
- package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
- package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
- package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
- package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
- package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
- package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
- package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
- package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
- package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
- package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
- package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
- package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
- package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
- package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
- package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
- package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
- package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
- package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
- package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
- package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
- package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
- package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
- package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
- package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
- package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
- package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
- package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
- package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
- package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
- package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
- package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
- package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
- package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
- package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
- package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
- package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
- package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
- package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
- package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
- package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
- package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
- package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
- package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
- package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
- package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
- package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
- package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
- package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
- package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
- package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
- package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
- package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
- package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
- package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
- package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
- package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
- package/web/fonts/LICENSE.txt +287 -0
- package/web/icon.svg +24 -0
- package/web/index.html +19 -0
- package/web/manifest.webmanifest +18 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust-backed notification service.
|
|
3
|
+
*
|
|
4
|
+
* Windows/macOS read the OS notification database through `dsh-probe
|
|
5
|
+
* notifications`. Linux runs `dsh-probe notifications watch`, which keeps a
|
|
6
|
+
* zbus monitor of `org.freedesktop.Notifications` alive and streams a snapshot
|
|
7
|
+
* per notification. There is no PowerShell/dbus-monitor fallback.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import type { ChildProcess } from 'node:child_process';
|
|
12
|
+
import * as readline from 'node:readline';
|
|
13
|
+
import { probeInfo, runProbe } from './probe.js';
|
|
14
|
+
import { isRecord } from './types.js';
|
|
15
|
+
import type { JsonRecord, NotificationApp, NotificationItem, NotificationPayload } from './types.js';
|
|
16
|
+
import { finiteNumber, safeString, serializeError } from './util.js';
|
|
17
|
+
|
|
18
|
+
const POLL_TTL_MS = 2500;
|
|
19
|
+
const ERROR_TTL_MS = 6000;
|
|
20
|
+
const WATCH_RESTART_MS = 5000;
|
|
21
|
+
|
|
22
|
+
/** Options of {@link normalizeNotificationPayload}; `limit` is clamped to 1..20. */
|
|
23
|
+
export interface NotificationNormalizeOptions {
|
|
24
|
+
limit?: number;
|
|
25
|
+
/** `errorPayload` also forwards this so the Linux watcher view stays live-only. */
|
|
26
|
+
liveOnly?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Options of {@link createNotificationService}. */
|
|
30
|
+
export interface NotificationServiceOptions {
|
|
31
|
+
limit?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A normalised item with every field this module populates. The shared
|
|
36
|
+
* `NotificationItem` keeps them optional because it also describes raw probe
|
|
37
|
+
* input, which is untrusted.
|
|
38
|
+
*/
|
|
39
|
+
interface NormalizedNotification extends NotificationItem {
|
|
40
|
+
id: string;
|
|
41
|
+
appKey: string;
|
|
42
|
+
appName: string;
|
|
43
|
+
title: string;
|
|
44
|
+
body: string;
|
|
45
|
+
avatar: string | null;
|
|
46
|
+
hasImage: boolean;
|
|
47
|
+
receivedAt: number;
|
|
48
|
+
isGroup: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Diagnostics snapshot returned by {@link NotificationService.status}. */
|
|
52
|
+
export interface NotificationServiceStatus {
|
|
53
|
+
/** `undefined` only when the cached payload carries no `available` flag. */
|
|
54
|
+
available: boolean | null | undefined;
|
|
55
|
+
reason: string | null;
|
|
56
|
+
count: number | null;
|
|
57
|
+
updatedAt: number | null;
|
|
58
|
+
liveOnly?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Host-side notification source consumed by the console service. */
|
|
62
|
+
export interface NotificationService {
|
|
63
|
+
getNotifications(): Promise<NotificationPayload>;
|
|
64
|
+
status(): NotificationServiceStatus;
|
|
65
|
+
dispose(): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeAvatar(value: unknown): string | null {
|
|
69
|
+
const avatar = typeof value === 'string' ? value.trim() : '';
|
|
70
|
+
return avatar.startsWith('data:image/') ? avatar : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a probe notification payload into the shape the console expects.
|
|
75
|
+
* Exported for tests; the probe normally returns this shape already.
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeNotificationPayload(
|
|
78
|
+
raw: unknown,
|
|
79
|
+
options: NotificationNormalizeOptions = {},
|
|
80
|
+
): NotificationPayload {
|
|
81
|
+
const limit = Math.max(1, Math.min(20, Number(options.limit) || 8));
|
|
82
|
+
const payload: JsonRecord = isRecord(raw) ? raw : {};
|
|
83
|
+
const available = payload.available !== false && payload.ok !== false;
|
|
84
|
+
const reason = safeString(payload.reason || payload.error || '', 800) || null;
|
|
85
|
+
const source = safeString(payload.source, 120) || 'dsh-probe';
|
|
86
|
+
const list: unknown[] = Array.isArray(payload.notifications) ? payload.notifications : [];
|
|
87
|
+
const notifications: NormalizedNotification[] = [];
|
|
88
|
+
for (const item of list) {
|
|
89
|
+
if (!isRecord(item)) continue;
|
|
90
|
+
const appKey = safeString(item.appKey || item.appName, 240).trim() || 'unknown';
|
|
91
|
+
const appName = safeString(item.appName || item.appKey, 120).trim() || appKey;
|
|
92
|
+
const title = safeString(item.title || appName, 160).trim();
|
|
93
|
+
const body = safeString(item.body, 260).trim();
|
|
94
|
+
if (!title && !body) continue;
|
|
95
|
+
const receivedAt: number | null = finiteNumber(item.receivedAt, null);
|
|
96
|
+
notifications.push({
|
|
97
|
+
id: safeString(item.id, 120) || `${receivedAt ?? Date.now()}-${notifications.length}`,
|
|
98
|
+
appKey,
|
|
99
|
+
appName,
|
|
100
|
+
title,
|
|
101
|
+
body,
|
|
102
|
+
avatar: normalizeAvatar(item.avatar),
|
|
103
|
+
hasImage: Boolean(item.hasImage),
|
|
104
|
+
receivedAt: receivedAt && receivedAt > 0 ? Math.round(receivedAt) : Date.now(),
|
|
105
|
+
isGroup: Boolean(item.isGroup),
|
|
106
|
+
});
|
|
107
|
+
if (notifications.length >= limit) break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const apps: NotificationApp[] = [];
|
|
111
|
+
const seen = new Map<string, NotificationApp>();
|
|
112
|
+
const appList: unknown[] = Array.isArray(payload.apps) ? payload.apps : [];
|
|
113
|
+
for (const app of appList) {
|
|
114
|
+
if (!isRecord(app)) continue;
|
|
115
|
+
const key = safeString(app.key, 240);
|
|
116
|
+
if (!key || seen.has(key)) continue;
|
|
117
|
+
const entry: NotificationApp = {
|
|
118
|
+
key,
|
|
119
|
+
name: safeString(app.name, 120) || key,
|
|
120
|
+
count: Math.max(0, Math.round(finiteNumber(app.count, 0) || 0)),
|
|
121
|
+
};
|
|
122
|
+
seen.set(key, entry);
|
|
123
|
+
apps.push(entry);
|
|
124
|
+
}
|
|
125
|
+
if (apps.length === 0) {
|
|
126
|
+
for (const item of notifications) {
|
|
127
|
+
const existing = seen.get(item.appKey);
|
|
128
|
+
if (existing) existing.count += 1;
|
|
129
|
+
else {
|
|
130
|
+
const entry: NotificationApp = { key: item.appKey, name: item.appName, count: 1 };
|
|
131
|
+
seen.set(item.appKey, entry);
|
|
132
|
+
apps.push(entry);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
apps.sort((left, right) => right.count - left.count);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
ok: available,
|
|
140
|
+
available,
|
|
141
|
+
reason,
|
|
142
|
+
source,
|
|
143
|
+
liveOnly: Boolean(payload.liveOnly),
|
|
144
|
+
notifications,
|
|
145
|
+
apps,
|
|
146
|
+
updatedAt: finiteNumber(payload.updatedAt, null) || Date.now(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function errorPayload(reason: unknown, extra: NotificationNormalizeOptions = {}): NotificationPayload {
|
|
151
|
+
return normalizeNotificationPayload(
|
|
152
|
+
{
|
|
153
|
+
ok: false,
|
|
154
|
+
available: false,
|
|
155
|
+
reason: serializeError(reason),
|
|
156
|
+
source: 'dsh-probe',
|
|
157
|
+
notifications: [],
|
|
158
|
+
apps: [],
|
|
159
|
+
...extra,
|
|
160
|
+
},
|
|
161
|
+
extra,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createNotificationService(options: NotificationServiceOptions = {}): NotificationService {
|
|
166
|
+
const limit = Math.max(1, Math.min(20, Number(options.limit) || 8));
|
|
167
|
+
const watchMode = process.platform === 'linux';
|
|
168
|
+
let cache: NotificationPayload | null = null;
|
|
169
|
+
let cacheAt = 0;
|
|
170
|
+
let child: ChildProcess | null = null;
|
|
171
|
+
let lines: readline.Interface | null = null;
|
|
172
|
+
let watchState: NotificationPayload | null = null;
|
|
173
|
+
let watchError: string | null = null;
|
|
174
|
+
let watchStartedAt = 0;
|
|
175
|
+
let restartAt = 0;
|
|
176
|
+
let disposed = false;
|
|
177
|
+
|
|
178
|
+
function unavailableReason(): string | null {
|
|
179
|
+
const info = probeInfo();
|
|
180
|
+
return info.available ? null : info.error || 'native probe unavailable';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function startWatch(): void {
|
|
184
|
+
if (child || disposed) return;
|
|
185
|
+
const info = probeInfo();
|
|
186
|
+
if (!info.available) {
|
|
187
|
+
watchError = info.error || 'native probe unavailable';
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let spawned: ChildProcess;
|
|
191
|
+
try {
|
|
192
|
+
spawned = spawn(info.binary, ['notifications', 'watch', '--limit', String(limit)], {
|
|
193
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
|
+
windowsHide: true,
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
watchError = serializeError(error);
|
|
198
|
+
restartAt = Date.now() + WATCH_RESTART_MS;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
child = spawned;
|
|
202
|
+
watchStartedAt = Date.now();
|
|
203
|
+
watchError = null;
|
|
204
|
+
const stdout = spawned.stdout;
|
|
205
|
+
if (stdout) {
|
|
206
|
+
const stream = readline.createInterface({ input: stdout });
|
|
207
|
+
lines = stream;
|
|
208
|
+
stream.on('line', (line: string) => {
|
|
209
|
+
const text = String(line).trim();
|
|
210
|
+
if (!text.startsWith('{')) return;
|
|
211
|
+
try {
|
|
212
|
+
const parsed: unknown = JSON.parse(text);
|
|
213
|
+
const state = normalizeNotificationPayload(parsed, { limit });
|
|
214
|
+
if (state.available) {
|
|
215
|
+
watchState = state;
|
|
216
|
+
watchError = null;
|
|
217
|
+
} else {
|
|
218
|
+
watchState = null;
|
|
219
|
+
watchError = state.reason || 'notification watcher unavailable';
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
/* ignore malformed lines */
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const stderr = spawned.stderr;
|
|
227
|
+
if (stderr) {
|
|
228
|
+
stderr.setEncoding('utf8');
|
|
229
|
+
stderr.on('data', (chunk: unknown) => {
|
|
230
|
+
const text = String(chunk).trim();
|
|
231
|
+
if (text) watchError = text.slice(0, 500);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
spawned.on('error', (error: Error) => {
|
|
235
|
+
watchError = serializeError(error);
|
|
236
|
+
watchState = null;
|
|
237
|
+
});
|
|
238
|
+
spawned.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
|
|
239
|
+
const wasStarted = Date.now() - watchStartedAt > 1000;
|
|
240
|
+
child = null;
|
|
241
|
+
watchState = null;
|
|
242
|
+
lines?.close();
|
|
243
|
+
lines = null;
|
|
244
|
+
if (disposed) return;
|
|
245
|
+
if (!watchError) {
|
|
246
|
+
watchError = `notification watcher exited (${signal || code || 'unknown'})`;
|
|
247
|
+
} else if (!wasStarted) {
|
|
248
|
+
watchError = `${watchError} (exited immediately)`;
|
|
249
|
+
}
|
|
250
|
+
restartAt = Date.now() + WATCH_RESTART_MS;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function readOneShot(): Promise<NotificationPayload> {
|
|
255
|
+
const reason = unavailableReason();
|
|
256
|
+
if (reason) return errorPayload(reason, { limit });
|
|
257
|
+
try {
|
|
258
|
+
const raw: unknown = await runProbe(['notifications', '--limit', String(limit)], { timeout: 9000 });
|
|
259
|
+
return normalizeNotificationPayload(raw, { limit });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return errorPayload(error, { limit });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function getNotifications(): Promise<NotificationPayload> {
|
|
266
|
+
if (!watchMode) {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const ttl = cache?.available ? POLL_TTL_MS : ERROR_TTL_MS;
|
|
269
|
+
if (cache && now - cacheAt < ttl) return { ...cache, cached: true };
|
|
270
|
+
cache = await readOneShot();
|
|
271
|
+
cacheAt = now;
|
|
272
|
+
return cache;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!child && !disposed && Date.now() >= restartAt) startWatch();
|
|
276
|
+
if (watchState) {
|
|
277
|
+
return { ...watchState, cached: false };
|
|
278
|
+
}
|
|
279
|
+
const reason = watchError
|
|
280
|
+
|| unavailableReason()
|
|
281
|
+
|| (child ? 'notification watcher starting' : 'notification watcher unavailable');
|
|
282
|
+
return errorPayload(reason, { limit, liveOnly: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function status(): NotificationServiceStatus {
|
|
286
|
+
if (!watchMode) {
|
|
287
|
+
return cache
|
|
288
|
+
? {
|
|
289
|
+
available: cache.available,
|
|
290
|
+
reason: cache.reason || null,
|
|
291
|
+
count: cache.notifications?.length ?? 0,
|
|
292
|
+
updatedAt: cache.updatedAt ?? null,
|
|
293
|
+
}
|
|
294
|
+
: { available: null, reason: null, count: null, updatedAt: null };
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
available: watchState ? true : watchError ? false : null,
|
|
298
|
+
reason: watchError || null,
|
|
299
|
+
count: watchState?.notifications?.length ?? 0,
|
|
300
|
+
updatedAt: watchState?.updatedAt ?? null,
|
|
301
|
+
liveOnly: true,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function dispose(): void {
|
|
306
|
+
disposed = true;
|
|
307
|
+
try {
|
|
308
|
+
lines?.close();
|
|
309
|
+
} catch {
|
|
310
|
+
/* ignore */
|
|
311
|
+
}
|
|
312
|
+
lines = null;
|
|
313
|
+
try {
|
|
314
|
+
child?.kill();
|
|
315
|
+
} catch {
|
|
316
|
+
/* ignore */
|
|
317
|
+
}
|
|
318
|
+
child = null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { getNotifications, status, dispose };
|
|
322
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance view backed by `dsh-probe performance`.
|
|
3
|
+
*
|
|
4
|
+
* CPU/memory/GPU/temperatures all come from Rust. Only when the sidecar is
|
|
5
|
+
* unavailable does Node provide a pure-JS CPU/memory fallback (`os.cpus()` /
|
|
6
|
+
* `os.totalmem()`); there is no WMI or PowerShell probe left.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { probeInfo, runProbe } from './probe.js';
|
|
11
|
+
import { isRecord, type JsonRecord } from './types.js';
|
|
12
|
+
import { clamp, finiteNumber, serializeError } from './util.js';
|
|
13
|
+
|
|
14
|
+
const PROBE_ERROR_TTL_MS = 8000;
|
|
15
|
+
|
|
16
|
+
/** CPU block: from Rust when the sidecar answers, otherwise from `os.cpus()`. */
|
|
17
|
+
export interface PerformanceCpu {
|
|
18
|
+
usage: number;
|
|
19
|
+
model: string;
|
|
20
|
+
cores: number;
|
|
21
|
+
physicalCores: number | null;
|
|
22
|
+
speed: number | null;
|
|
23
|
+
baseSpeed: number | null;
|
|
24
|
+
uptime: number;
|
|
25
|
+
source: 'node' | 'rust';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** System memory in bytes, plus the usage percentage. */
|
|
29
|
+
export interface PerformanceMemory {
|
|
30
|
+
total: number;
|
|
31
|
+
used: number;
|
|
32
|
+
free: number;
|
|
33
|
+
percent: number;
|
|
34
|
+
source: 'node' | 'rust';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* GPU block. `driver`, `adapters` and the memory/utilization readings are only
|
|
39
|
+
* present when the probe reported a GPU; the fallback shape carries just
|
|
40
|
+
* `name`, `ok`, `error` and `source`.
|
|
41
|
+
*/
|
|
42
|
+
export interface PerformanceGpu {
|
|
43
|
+
name: string;
|
|
44
|
+
ok: boolean;
|
|
45
|
+
error: string | null;
|
|
46
|
+
source: string;
|
|
47
|
+
driver?: string;
|
|
48
|
+
adapters?: unknown[];
|
|
49
|
+
utilization?: number | null;
|
|
50
|
+
memoryUsed?: number | null;
|
|
51
|
+
memoryTotal?: number | null;
|
|
52
|
+
memoryPercent?: number | null;
|
|
53
|
+
coreClockMhz?: number | null;
|
|
54
|
+
coreClockMaxMhz?: number | null;
|
|
55
|
+
memoryClockMhz?: number | null;
|
|
56
|
+
powerPercent?: number | null;
|
|
57
|
+
fanRpm?: number | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One temperature sensor reading in degrees Celsius. */
|
|
61
|
+
export interface PerformanceTemperature {
|
|
62
|
+
name: string;
|
|
63
|
+
celsius: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Whether the sidecar answered, and why it did not. */
|
|
67
|
+
export interface PerformanceProbeState {
|
|
68
|
+
used: boolean;
|
|
69
|
+
error: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Sampling result handed to the browser. */
|
|
73
|
+
export interface PerformancePayload {
|
|
74
|
+
cpu: PerformanceCpu;
|
|
75
|
+
memory: PerformanceMemory;
|
|
76
|
+
gpu: PerformanceGpu;
|
|
77
|
+
temperatures: PerformanceTemperature[];
|
|
78
|
+
probe: PerformanceProbeState;
|
|
79
|
+
sampledAt: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Diagnostic snapshot returned by `status()`. */
|
|
83
|
+
export interface PerformanceStatus {
|
|
84
|
+
cpu: number;
|
|
85
|
+
probeError: string | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PerformanceService {
|
|
89
|
+
sample(force?: boolean): Promise<PerformancePayload>;
|
|
90
|
+
status(): PerformanceStatus;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function round(value: number, digits = 1): number {
|
|
94
|
+
const factor = 10 ** digits;
|
|
95
|
+
return Math.round(value * factor) / factor;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cpuTimes(): { idle: number; total: number } {
|
|
99
|
+
let idle = 0;
|
|
100
|
+
let total = 0;
|
|
101
|
+
for (const cpu of os.cpus()) {
|
|
102
|
+
idle += cpu.times.idle;
|
|
103
|
+
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
104
|
+
}
|
|
105
|
+
return { idle, total };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mapProbeGpu(gpu: JsonRecord): PerformanceGpu {
|
|
109
|
+
const total = finiteNumber(gpu.memoryTotal, null);
|
|
110
|
+
const used = finiteNumber(gpu.memoryUsed, null);
|
|
111
|
+
const adapters: unknown[] = Array.isArray(gpu.adapters) ? gpu.adapters : [];
|
|
112
|
+
return {
|
|
113
|
+
name: typeof gpu.name === 'string' && gpu.name ? gpu.name : 'GPU',
|
|
114
|
+
driver: typeof gpu.driver === 'string' && gpu.driver ? gpu.driver : '',
|
|
115
|
+
adapters,
|
|
116
|
+
utilization: finiteNumber(gpu.utilization, null),
|
|
117
|
+
memoryUsed: used,
|
|
118
|
+
memoryTotal: total,
|
|
119
|
+
memoryPercent: finiteNumber(gpu.memoryPercent, null),
|
|
120
|
+
coreClockMhz: finiteNumber(gpu.coreClockMhz, null),
|
|
121
|
+
coreClockMaxMhz: finiteNumber(gpu.coreClockMaxMhz, null),
|
|
122
|
+
memoryClockMhz: finiteNumber(gpu.memoryClockMhz, null),
|
|
123
|
+
powerPercent: finiteNumber(gpu.powerPercent, null),
|
|
124
|
+
fanRpm: finiteNumber(gpu.fanRpm, null),
|
|
125
|
+
ok: gpu.ok !== false,
|
|
126
|
+
error: typeof gpu.error === 'string' && gpu.error ? gpu.error : null,
|
|
127
|
+
source: typeof gpu.source === 'string' && gpu.source ? gpu.source : 'rust',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createPerformanceService(): PerformanceService {
|
|
132
|
+
let previous = cpuTimes();
|
|
133
|
+
let cpuUsage = 0;
|
|
134
|
+
let probeError: string | null = null;
|
|
135
|
+
let probeAt = 0;
|
|
136
|
+
let inFlight: Promise<JsonRecord | null> | null = null;
|
|
137
|
+
|
|
138
|
+
function sampleNodeCpu(): PerformanceCpu {
|
|
139
|
+
const current = cpuTimes();
|
|
140
|
+
const idle = current.idle - previous.idle;
|
|
141
|
+
const total = current.total - previous.total;
|
|
142
|
+
previous = current;
|
|
143
|
+
if (total > 0) {
|
|
144
|
+
cpuUsage = clamp((1 - idle / total) * 100, 0, 100);
|
|
145
|
+
}
|
|
146
|
+
const cpus = os.cpus();
|
|
147
|
+
const baseSpeed = finiteNumber(cpus[0]?.speed, null);
|
|
148
|
+
return {
|
|
149
|
+
usage: round(cpuUsage),
|
|
150
|
+
model: cpus[0]?.model || 'CPU',
|
|
151
|
+
cores: cpus.length,
|
|
152
|
+
physicalCores: null,
|
|
153
|
+
speed: baseSpeed,
|
|
154
|
+
baseSpeed,
|
|
155
|
+
uptime: os.uptime(),
|
|
156
|
+
source: 'node',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nodeMemory(): PerformanceMemory {
|
|
161
|
+
const total = os.totalmem();
|
|
162
|
+
const free = os.freemem();
|
|
163
|
+
return {
|
|
164
|
+
total,
|
|
165
|
+
used: total - free,
|
|
166
|
+
free,
|
|
167
|
+
percent: total ? round(((total - free) / total) * 100) : 0,
|
|
168
|
+
source: 'node',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readProbePerformance(): Promise<JsonRecord | null> {
|
|
173
|
+
if (!probeInfo().available) return null;
|
|
174
|
+
if (probeError && Date.now() - probeAt < PROBE_ERROR_TTL_MS) return null;
|
|
175
|
+
if (inFlight) return inFlight;
|
|
176
|
+
inFlight = (async (): Promise<JsonRecord | null> => {
|
|
177
|
+
try {
|
|
178
|
+
const raw = await runProbe(['performance'], { timeout: 12_000 });
|
|
179
|
+
const result = isRecord(raw) ? raw : null;
|
|
180
|
+
if (result && result.ok === false) {
|
|
181
|
+
const message = typeof result.error === 'string' && result.error
|
|
182
|
+
? result.error
|
|
183
|
+
: 'dsh-probe performance failed';
|
|
184
|
+
throw new Error(message);
|
|
185
|
+
}
|
|
186
|
+
probeError = null;
|
|
187
|
+
return result;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
probeError = serializeError(error);
|
|
190
|
+
probeAt = Date.now();
|
|
191
|
+
return null;
|
|
192
|
+
} finally {
|
|
193
|
+
inFlight = null;
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
return inFlight;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function sample(force?: boolean): Promise<PerformancePayload> {
|
|
200
|
+
const probe = await readProbePerformance();
|
|
201
|
+
const probeCpu = probe && isRecord(probe.cpu) ? probe.cpu : null;
|
|
202
|
+
const probeMemory = probe && isRecord(probe.memory) ? probe.memory : null;
|
|
203
|
+
|
|
204
|
+
const cpu: PerformanceCpu = probeCpu
|
|
205
|
+
? {
|
|
206
|
+
usage: finiteNumber(probeCpu.usage, 0) ?? 0,
|
|
207
|
+
model: typeof probeCpu.model === 'string' && probeCpu.model ? probeCpu.model : 'CPU',
|
|
208
|
+
cores: finiteNumber(probeCpu.cores, null) || os.cpus().length,
|
|
209
|
+
physicalCores: finiteNumber(probeCpu.physicalCores, null),
|
|
210
|
+
speed: finiteNumber(probeCpu.speedMhz, null),
|
|
211
|
+
baseSpeed: finiteNumber(probeCpu.baseMhz, null),
|
|
212
|
+
uptime: finiteNumber(probeCpu.uptime, os.uptime()) ?? os.uptime(),
|
|
213
|
+
source: 'rust',
|
|
214
|
+
}
|
|
215
|
+
: sampleNodeCpu();
|
|
216
|
+
|
|
217
|
+
let memory: PerformanceMemory;
|
|
218
|
+
if (probeMemory) {
|
|
219
|
+
const total = finiteNumber(probeMemory.total, 0) ?? 0;
|
|
220
|
+
const used = finiteNumber(probeMemory.used, 0) ?? 0;
|
|
221
|
+
const free = finiteNumber(probeMemory.free, 0) ?? 0;
|
|
222
|
+
memory = {
|
|
223
|
+
total,
|
|
224
|
+
used,
|
|
225
|
+
free,
|
|
226
|
+
percent: finiteNumber(probeMemory.percent, null) ?? (total ? round((used / total) * 100) : 0),
|
|
227
|
+
source: 'rust',
|
|
228
|
+
};
|
|
229
|
+
} else {
|
|
230
|
+
memory = nodeMemory();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let gpu: PerformanceGpu;
|
|
234
|
+
const probeGpu = probe && isRecord(probe.gpu) ? probe.gpu : null;
|
|
235
|
+
if (probeGpu) {
|
|
236
|
+
gpu = mapProbeGpu(probeGpu);
|
|
237
|
+
} else {
|
|
238
|
+
gpu = {
|
|
239
|
+
name: 'GPU',
|
|
240
|
+
ok: false,
|
|
241
|
+
error: probe
|
|
242
|
+
? '当前平台没有可用的 GPU 读数'
|
|
243
|
+
: probeInfo().error || probeError || 'native probe unavailable',
|
|
244
|
+
source: 'none',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const temperatures: PerformanceTemperature[] = [];
|
|
249
|
+
const seen = new Set<string>();
|
|
250
|
+
const candidates: unknown[] = probe && Array.isArray(probe.temperatures) ? probe.temperatures : [];
|
|
251
|
+
for (const item of candidates) {
|
|
252
|
+
const record = isRecord(item) ? item : null;
|
|
253
|
+
const value = finiteNumber(record?.celsius, null);
|
|
254
|
+
if (value === null || value < 5 || value > 125) continue;
|
|
255
|
+
const name = record && record.name ? String(record.name) : '设备';
|
|
256
|
+
const key = name.toLowerCase();
|
|
257
|
+
if (seen.has(key)) continue;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
temperatures.push({ name, celsius: round(value) });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
cpu,
|
|
264
|
+
memory,
|
|
265
|
+
gpu,
|
|
266
|
+
temperatures,
|
|
267
|
+
probe: {
|
|
268
|
+
used: Boolean(probe),
|
|
269
|
+
error: probe ? null : probeInfo().error || probeError || null,
|
|
270
|
+
},
|
|
271
|
+
sampledAt: Date.now(),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
sample,
|
|
277
|
+
status: () => ({ cpu: cpuUsage, probeError }),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Wires the Rust probe into the platform-independent service interfaces. */
|
|
2
|
+
|
|
3
|
+
import { createNotificationService } from '../notifications.js';
|
|
4
|
+
import { createProbeMediaAdapter } from '../probe-adapter.js';
|
|
5
|
+
|
|
6
|
+
const LABELS: Record<string, string> = {
|
|
7
|
+
win32: 'Windows',
|
|
8
|
+
darwin: 'macOS',
|
|
9
|
+
linux: 'Linux',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface PlatformAdaptersOptions {
|
|
13
|
+
limit?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PlatformAdapters {
|
|
17
|
+
platform: NodeJS.Platform;
|
|
18
|
+
label: string;
|
|
19
|
+
notifications: ReturnType<typeof createNotificationService>;
|
|
20
|
+
media: ReturnType<typeof createProbeMediaAdapter>;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Media, volume and notifications all come from `dsh-probe`. There is no
|
|
26
|
+
* per-platform JavaScript fallback: a missing/unsupported probe is surfaced as
|
|
27
|
+
* an error payload by the adapters themselves.
|
|
28
|
+
*/
|
|
29
|
+
export function createPlatformAdapters(options: PlatformAdaptersOptions = {}): PlatformAdapters {
|
|
30
|
+
const platform = process.platform;
|
|
31
|
+
const label = LABELS[platform] || platform;
|
|
32
|
+
const notifications = createNotificationService({ limit: options.limit });
|
|
33
|
+
const media = createProbeMediaAdapter({ label });
|
|
34
|
+
return {
|
|
35
|
+
platform,
|
|
36
|
+
label,
|
|
37
|
+
notifications,
|
|
38
|
+
media,
|
|
39
|
+
dispose() {
|
|
40
|
+
notifications.dispose?.();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|