@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.2

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.
Files changed (373) hide show
  1. package/AGENTS.md +556 -0
  2. package/INSTALLATION.md +310 -0
  3. package/LICENSE +21 -0
  4. package/README.md +195 -2
  5. package/client.js +3 -0
  6. package/components.json +20 -0
  7. package/cordis.patch.yml +3 -0
  8. package/docs/README.en.md +196 -0
  9. package/docs/screenshots/en/overview.png +0 -0
  10. package/docs/screenshots/en/question.png +0 -0
  11. package/docs/screenshots/zh/overview.png +0 -0
  12. package/docs/screenshots/zh/question.png +0 -0
  13. package/icon.svg +24 -0
  14. package/index.js +2 -0
  15. package/lib/account.js +1 -0
  16. package/lib/app-mapping.js +1 -0
  17. package/lib/i18n.js +1 -0
  18. package/lib/media-enrich.js +1 -0
  19. package/lib/notifications.js +1 -0
  20. package/lib/performance.js +1 -0
  21. package/lib/platform/index.js +1 -0
  22. package/lib/probe-adapter.js +1 -0
  23. package/lib/probe-daemon.js +1 -0
  24. package/lib/probe.js +1 -0
  25. package/lib/qr.js +1 -0
  26. package/lib/runtime.js +1 -0
  27. package/lib/service.js +1 -0
  28. package/lib/sessions.js +1 -0
  29. package/lib/settings.js +1 -0
  30. package/lib/types.js +1 -0
  31. package/lib/util.js +1 -0
  32. package/lib/weather.js +1 -0
  33. package/lib/ws.js +1 -0
  34. package/locale/en.json +6 -0
  35. package/locale/zh.json +6 -0
  36. package/package.json +118 -4
  37. package/scripts/browser-shot.mjs +217 -0
  38. package/scripts/build-dsh.mjs +228 -0
  39. package/scripts/build-rust.mjs +319 -0
  40. package/scripts/build-web.mjs +17 -0
  41. package/scripts/check-npm-package.mjs +322 -0
  42. package/scripts/mock-preview.py +574 -0
  43. package/scripts/screenshot-compose.py +89 -0
  44. package/scripts/screenshot-device.sh +75 -0
  45. package/src/dsh/client/api.ts +68 -0
  46. package/src/dsh/client/i18n.ts +573 -0
  47. package/src/dsh/client/icons.ts +56 -0
  48. package/src/dsh/client/index.ts +95 -0
  49. package/src/dsh/client/logic.ts +310 -0
  50. package/src/dsh/client/panel.ts +483 -0
  51. package/src/dsh/client/sections/apps.ts +40 -0
  52. package/src/dsh/client/sections/balance.ts +38 -0
  53. package/src/dsh/client/sections/connection.ts +97 -0
  54. package/src/dsh/client/sections/notifications.ts +43 -0
  55. package/src/dsh/client/sections/pomodoro.ts +184 -0
  56. package/src/dsh/client/sections/preferences.ts +138 -0
  57. package/src/dsh/client/sections/system.ts +120 -0
  58. package/src/dsh/client/sections/weather.ts +276 -0
  59. package/src/dsh/client/styles.ts +1119 -0
  60. package/src/dsh/client/types.ts +366 -0
  61. package/src/dsh/client/ui.ts +142 -0
  62. package/src/dsh/index.ts +108 -0
  63. package/src/dsh/lib/account.ts +520 -0
  64. package/src/dsh/lib/app-mapping.ts +366 -0
  65. package/src/dsh/lib/i18n.ts +82 -0
  66. package/src/dsh/lib/media-enrich.ts +202 -0
  67. package/src/dsh/lib/notifications.ts +322 -0
  68. package/src/dsh/lib/performance.ts +279 -0
  69. package/src/dsh/lib/platform/index.ts +43 -0
  70. package/src/dsh/lib/probe-adapter.ts +275 -0
  71. package/src/dsh/lib/probe-daemon.ts +359 -0
  72. package/src/dsh/lib/probe.ts +395 -0
  73. package/src/dsh/lib/qr.ts +42 -0
  74. package/src/dsh/lib/runtime.ts +126 -0
  75. package/src/dsh/lib/service.ts +1645 -0
  76. package/src/dsh/lib/sessions.ts +907 -0
  77. package/src/dsh/lib/settings.ts +468 -0
  78. package/src/dsh/lib/types.ts +231 -0
  79. package/src/dsh/lib/util.ts +115 -0
  80. package/src/dsh/lib/weather.ts +532 -0
  81. package/src/dsh/lib/ws.ts +328 -0
  82. package/src/dsh/types/qrcode-generator.d.ts +16 -0
  83. package/src/rust/Cargo.lock +1431 -0
  84. package/src/rust/Cargo.toml +68 -0
  85. package/src/rust/src/cli.rs +233 -0
  86. package/src/rust/src/daemon.rs +140 -0
  87. package/src/rust/src/location/linux.rs +118 -0
  88. package/src/rust/src/location/macos.rs +26 -0
  89. package/src/rust/src/location/mod.rs +170 -0
  90. package/src/rust/src/location/windows.rs +205 -0
  91. package/src/rust/src/main.rs +42 -0
  92. package/src/rust/src/media/catalog.rs +454 -0
  93. package/src/rust/src/media/linux.rs +350 -0
  94. package/src/rust/src/media/macos/accessibility.rs +59 -0
  95. package/src/rust/src/media/macos.rs +366 -0
  96. package/src/rust/src/media/mod.rs +298 -0
  97. package/src/rust/src/media/vendor/apple_music.rs +90 -0
  98. package/src/rust/src/media/vendor/common.rs +186 -0
  99. package/src/rust/src/media/vendor/kugou.rs +52 -0
  100. package/src/rust/src/media/vendor/kuwo.rs +49 -0
  101. package/src/rust/src/media/vendor/mod.rs +75 -0
  102. package/src/rust/src/media/vendor/netease.rs +1715 -0
  103. package/src/rust/src/media/vendor/qishui.rs +36 -0
  104. package/src/rust/src/media/vendor/qqmusic.rs +33 -0
  105. package/src/rust/src/media/vendor/spotify.rs +46 -0
  106. package/src/rust/src/media/vendor/wesing.rs +39 -0
  107. package/src/rust/src/media/vendor/youtube_music.rs +49 -0
  108. package/src/rust/src/media/windows/accessibility.rs +85 -0
  109. package/src/rust/src/media/windows/audio.rs +116 -0
  110. package/src/rust/src/media/windows/private.rs +291 -0
  111. package/src/rust/src/media/windows.rs +775 -0
  112. package/src/rust/src/notifications/linux.rs +276 -0
  113. package/src/rust/src/notifications/macos.rs +382 -0
  114. package/src/rust/src/notifications/mod.rs +244 -0
  115. package/src/rust/src/notifications/windows.rs +565 -0
  116. package/src/rust/src/perf/common.rs +109 -0
  117. package/src/rust/src/perf/linux.rs +282 -0
  118. package/src/rust/src/perf/macos.rs +28 -0
  119. package/src/rust/src/perf/mod.rs +157 -0
  120. package/src/rust/src/perf/windows.rs +981 -0
  121. package/src/rust/src/util.rs +360 -0
  122. package/src/rust/src/volume/linux.rs +195 -0
  123. package/src/rust/src/volume/macos.rs +54 -0
  124. package/src/rust/src/volume/mod.rs +116 -0
  125. package/src/rust/src/volume/windows.rs +75 -0
  126. package/src/rust/tests/cli.rs +79 -0
  127. package/src/rust/tests/daemon.rs +107 -0
  128. package/src/svelte/App.svelte +385 -0
  129. package/src/svelte/index.html +18 -0
  130. package/src/svelte/lib/billing.svelte.ts +228 -0
  131. package/src/svelte/lib/components/BalanceCard.svelte +173 -0
  132. package/src/svelte/lib/components/ClockCard.svelte +183 -0
  133. package/src/svelte/lib/components/CountdownCard.svelte +261 -0
  134. package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
  135. package/src/svelte/lib/components/EmptyState.svelte +34 -0
  136. package/src/svelte/lib/components/GpuCard.svelte +245 -0
  137. package/src/svelte/lib/components/HomePage.svelte +34 -0
  138. package/src/svelte/lib/components/MusicControls.svelte +256 -0
  139. package/src/svelte/lib/components/MusicCover.svelte +88 -0
  140. package/src/svelte/lib/components/MusicPage.svelte +116 -0
  141. package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
  142. package/src/svelte/lib/components/Panel.svelte +22 -0
  143. package/src/svelte/lib/components/PerformancePage.svelte +55 -0
  144. package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
  145. package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
  146. package/src/svelte/lib/components/RadialGauge.svelte +99 -0
  147. package/src/svelte/lib/components/Rail.svelte +217 -0
  148. package/src/svelte/lib/components/SectionHeader.svelte +40 -0
  149. package/src/svelte/lib/components/SeekBar.svelte +421 -0
  150. package/src/svelte/lib/components/SessionList.svelte +364 -0
  151. package/src/svelte/lib/components/SessionStats.svelte +98 -0
  152. package/src/svelte/lib/components/SessionsPage.svelte +61 -0
  153. package/src/svelte/lib/components/SpendCard.svelte +105 -0
  154. package/src/svelte/lib/components/ThermalCard.svelte +249 -0
  155. package/src/svelte/lib/components/Toast.svelte +32 -0
  156. package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
  157. package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
  158. package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
  159. package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
  160. package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
  161. package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
  162. package/src/svelte/lib/components/ui/alert/index.ts +17 -0
  163. package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
  164. package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
  165. package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
  166. package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
  167. package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
  168. package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
  169. package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
  170. package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
  171. package/src/svelte/lib/components/ui/badge/index.ts +2 -0
  172. package/src/svelte/lib/components/ui/button/button.svelte +82 -0
  173. package/src/svelte/lib/components/ui/button/index.ts +17 -0
  174. package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
  175. package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
  176. package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
  177. package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
  178. package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
  179. package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
  180. package/src/svelte/lib/components/ui/card/card.svelte +22 -0
  181. package/src/svelte/lib/components/ui/card/index.ts +25 -0
  182. package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
  183. package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
  184. package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
  185. package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
  186. package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
  187. package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
  188. package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  189. package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
  190. package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
  191. package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
  192. package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
  193. package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
  194. package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
  195. package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
  196. package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
  197. package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
  198. package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
  199. package/src/svelte/lib/components/ui/empty/index.ts +22 -0
  200. package/src/svelte/lib/components/ui/input/index.ts +7 -0
  201. package/src/svelte/lib/components/ui/input/input.svelte +48 -0
  202. package/src/svelte/lib/components/ui/progress/index.ts +7 -0
  203. package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
  204. package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
  205. package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
  206. package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
  207. package/src/svelte/lib/components/ui/separator/index.ts +7 -0
  208. package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
  209. package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
  210. package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
  211. package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
  212. package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
  213. package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  214. package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
  215. package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  216. package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  217. package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
  218. package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  219. package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
  220. package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
  221. package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
  222. package/src/svelte/lib/components/ui/slider/index.ts +7 -0
  223. package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
  224. package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
  225. package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
  226. package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  227. package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  228. package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  229. package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
  230. package/src/svelte/lib/countdown.svelte.ts +252 -0
  231. package/src/svelte/lib/format.ts +199 -0
  232. package/src/svelte/lib/i18n.svelte.ts +583 -0
  233. package/src/svelte/lib/media.ts +32 -0
  234. package/src/svelte/lib/pages.ts +18 -0
  235. package/src/svelte/lib/pomodoro.svelte.ts +223 -0
  236. package/src/svelte/lib/session-tree.ts +80 -0
  237. package/src/svelte/lib/state.svelte.ts +359 -0
  238. package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
  239. package/src/svelte/lib/types.ts +306 -0
  240. package/src/svelte/lib/utils.ts +8 -0
  241. package/src/svelte/lib/weather.ts +84 -0
  242. package/src/svelte/lib/ws.ts +104 -0
  243. package/src/svelte/main.ts +13 -0
  244. package/src/svelte/styles/global.css +315 -0
  245. package/src/svelte/vite-env.d.ts +12 -0
  246. package/static/fonts/LICENSE.txt +287 -0
  247. package/static/icon.svg +24 -0
  248. package/static/manifest.webmanifest +18 -0
  249. package/svelte.config.js +5 -0
  250. package/tsconfig.dsh-client.json +18 -0
  251. package/tsconfig.dsh.json +21 -0
  252. package/tsconfig.json +20 -0
  253. package/vite.config.js +31 -0
  254. package/web/assets/index-DLdSfnJ3.css +1 -0
  255. package/web/assets/index-YNE8X0Uy.js +6 -0
  256. package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  257. package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  258. package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  259. package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  260. package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  261. package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  262. package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  263. package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  264. package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  265. package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  266. package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  267. package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  268. package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  269. package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
  270. package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
  271. package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
  272. package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
  273. package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
  274. package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
  275. package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
  276. package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
  277. package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
  278. package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
  279. package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
  280. package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
  281. package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
  282. package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
  283. package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
  284. package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
  285. package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
  286. package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
  287. package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
  288. package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
  289. package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
  290. package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
  291. package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
  292. package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
  293. package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
  294. package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
  295. package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
  296. package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
  297. package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
  298. package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
  299. package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
  300. package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
  301. package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
  302. package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
  303. package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
  304. package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
  305. package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
  306. package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
  307. package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
  308. package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
  309. package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
  310. package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
  311. package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
  312. package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
  313. package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
  314. package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
  315. package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
  316. package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
  317. package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
  318. package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
  319. package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
  320. package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
  321. package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
  322. package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
  323. package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
  324. package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
  325. package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
  326. package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
  327. package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
  328. package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
  329. package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
  330. package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
  331. package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
  332. package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
  333. package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
  334. package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
  335. package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
  336. package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
  337. package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
  338. package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
  339. package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
  340. package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
  341. package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
  342. package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
  343. package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
  344. package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
  345. package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
  346. package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
  347. package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
  348. package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
  349. package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
  350. package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
  351. package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
  352. package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
  353. package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
  354. package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
  355. package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
  356. package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
  357. package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
  358. package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
  359. package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
  360. package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
  361. package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
  362. package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
  363. package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
  364. package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
  365. package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
  366. package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
  367. package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
  368. package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
  369. package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
  370. package/web/fonts/LICENSE.txt +287 -0
  371. package/web/icon.svg +24 -0
  372. package/web/index.html +19 -0
  373. package/web/manifest.webmanifest +18 -0
@@ -0,0 +1,981 @@
1
+ //! Windows performance probe.
2
+ //!
3
+ //! CPU/memory come from sysinfo. The current core clock uses the PDH
4
+ //! `% Processor Performance` counter with a `CallNtPowerInformation` fallback.
5
+ //! GPU name/VRAM come from DXGI (registry as a fallback), GPU utilization and
6
+ //! dedicated memory from PDH GPU performance counters, and temperatures from the
7
+ //! PDH thermal-zone counters. GPU clocks, die temperature, power and fan speed
8
+ //! come from the WDDM performance data (`D3DKMTQueryAdapterInfo`), which is the
9
+ //! same source Task Manager uses. None of this goes through WMI or PowerShell.
10
+
11
+ use std::cell::RefCell;
12
+ use std::collections::{HashMap, HashSet};
13
+ use std::mem::size_of;
14
+ use std::time::{Duration, Instant};
15
+
16
+ use windows::core::{HSTRING, PCWSTR};
17
+ use windows::Wdk::Graphics::Direct3D::{
18
+ D3DKMTCloseAdapter, D3DKMTOpenAdapterFromLuid, D3DKMTQueryAdapterInfo, D3DKMT_ADAPTER_PERFDATA,
19
+ D3DKMT_CLOSEADAPTER, D3DKMT_NODEMETADATA, D3DKMT_NODE_PERFDATA, D3DKMT_OPENADAPTERFROMLUID,
20
+ D3DKMT_QUERYADAPTERINFO, DXGK_ENGINE_TYPE_3D, KMTQAITYPE_ADAPTERPERFDATA,
21
+ KMTQAITYPE_NODEMETADATA, KMTQAITYPE_NODEPERFDATA, KMTQUERYADAPTERINFOTYPE,
22
+ };
23
+ use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
24
+ use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1};
25
+ use windows::Win32::Storage::FileSystem::{
26
+ CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
27
+ };
28
+ use windows::Win32::System::Performance::{
29
+ PdhAddEnglishCounterW, PdhCloseQuery, PdhCollectQueryData, PdhGetFormattedCounterArrayW,
30
+ PdhGetFormattedCounterValue, PdhOpenQueryW, PDH_FMT_COUNTERVALUE, PDH_FMT_COUNTERVALUE_ITEM_W,
31
+ PDH_FMT_DOUBLE, PDH_HCOUNTER, PDH_HQUERY,
32
+ };
33
+ use windows::Win32::System::Power::{
34
+ CallNtPowerInformation, ProcessorInformation, PROCESSOR_POWER_INFORMATION,
35
+ };
36
+ use windows::Win32::System::IO::DeviceIoControl;
37
+ use winreg::enums::{HKEY_LOCAL_MACHINE, REG_BINARY, REG_QWORD};
38
+ use winreg::RegKey;
39
+
40
+ use super::{common, Gpu, Performance, Temperature};
41
+ use crate::util;
42
+
43
+ pub const BACKEND: &str = "win32-dxgi-pdh-d3dkmt+sysinfo";
44
+
45
+ /// `\\Processor Information(_Total)\\% Processor Performance`, the same counter
46
+ /// the old PowerShell probe used (`PercentProcessorPerformance`).
47
+ const PROCESSOR_PERFORMANCE_COUNTER: &str =
48
+ "\\Processor Information(_Total)\\% Processor Performance";
49
+ const GPU_ENGINE_COUNTER: &str = "\\GPU Engine(*)\\Utilization Percentage";
50
+ const GPU_MEMORY_COUNTER: &str = "\\GPU Adapter Memory(*)\\Dedicated Usage";
51
+ const THERMAL_PRECISION_COUNTER: &str = "\\Thermal Zone Information(*)\\High Precision Temperature";
52
+ const THERMAL_COUNTER: &str = "\\Thermal Zone Information(*)\\Temperature";
53
+ const PDH_MORE_DATA: u32 = 0x8000_07D2;
54
+ /// Rate counters need two collects; skip a re-collect when requests arrive in a
55
+ /// burst so the second read reuses the last valid value instead of sleeping.
56
+ const PDH_MIN_INTERVAL: Duration = Duration::from_millis(200);
57
+ /// After a long idle gap the previous rate baseline is too old; take a fresh
58
+ /// two-sample window instead of averaging across the whole idle period.
59
+ const PDH_RESAMPLE_AFTER: Duration = Duration::from_secs(5);
60
+ const PDH_WARMUP: Duration = Duration::from_millis(150);
61
+ const GPU_CLASS_KEY: &str =
62
+ "SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}";
63
+
64
+ fn registry_cpu() -> (Option<String>, Option<f64>) {
65
+ let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE)
66
+ .open_subkey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0")
67
+ else {
68
+ return (None, None);
69
+ };
70
+ let name = hklm
71
+ .get_value::<String, _>("ProcessorNameString")
72
+ .ok()
73
+ .map(|name| name.trim().to_string())
74
+ .filter(|name| !name.is_empty());
75
+ let max_clock = hklm
76
+ .get_value::<u32, _>("MaxClockSpeed")
77
+ .ok()
78
+ .map(|value| value as f64)
79
+ .filter(|value| *value > 0.0);
80
+ (name, max_clock)
81
+ }
82
+
83
+ fn power_frequency() -> Option<(f64, f64)> {
84
+ let count = std::thread::available_parallelism()
85
+ .map(|count| count.get())
86
+ .unwrap_or(1)
87
+ .max(1);
88
+ let mut info: Vec<PROCESSOR_POWER_INFORMATION> =
89
+ (0..count).map(|_| unsafe { std::mem::zeroed() }).collect();
90
+ let bytes = (info.len() * size_of::<PROCESSOR_POWER_INFORMATION>()) as u32;
91
+ let status = unsafe {
92
+ CallNtPowerInformation(
93
+ ProcessorInformation,
94
+ None,
95
+ 0,
96
+ Some(info.as_mut_ptr() as *mut core::ffi::c_void),
97
+ bytes,
98
+ )
99
+ };
100
+ if status.0 < 0 {
101
+ return None;
102
+ }
103
+ let mut current = 0.0;
104
+ let mut max = 0.0;
105
+ let mut seen = 0_u32;
106
+ for entry in &info {
107
+ if entry.MaxMhz > 0 {
108
+ max += entry.MaxMhz as f64;
109
+ }
110
+ if entry.CurrentMhz > 0 {
111
+ current += entry.CurrentMhz as f64;
112
+ seen += 1;
113
+ }
114
+ }
115
+ let current = if seen > 0 { current / seen as f64 } else { 0.0 };
116
+ let max = if count > 0 { max / count as f64 } else { 0.0 };
117
+ if current <= 0.0 && max <= 0.0 {
118
+ return None;
119
+ }
120
+ Some((util::round1(current), util::round1(max)))
121
+ }
122
+
123
+ /// A PDH query kept open across samples. Opening the query and resolving the
124
+ /// counter path is expensive for wildcard GPU counters, and the rate counters
125
+ /// need a previous sample anyway; both costs disappear after the first request.
126
+ struct PdhCounter {
127
+ query: PDH_HQUERY,
128
+ counter: PDH_HCOUNTER,
129
+ last_collect: Option<Instant>,
130
+ }
131
+
132
+ impl PdhCounter {
133
+ fn open(path: &str) -> Result<Self, String> {
134
+ unsafe {
135
+ let mut query = PDH_HQUERY(std::ptr::null_mut());
136
+ if PdhOpenQueryW(PCWSTR::null(), 0, &mut query) != 0 {
137
+ return Err("PdhOpenQueryW failed".to_string());
138
+ }
139
+ let wide = HSTRING::from(path);
140
+ let mut counter = PDH_HCOUNTER(std::ptr::null_mut());
141
+ let status = PdhAddEnglishCounterW(query, &wide, 0, &mut counter);
142
+ if status != 0 {
143
+ let _ = PdhCloseQuery(query);
144
+ return Err(format!("counter unavailable (status {status})"));
145
+ }
146
+ Ok(Self {
147
+ query,
148
+ counter,
149
+ last_collect: None,
150
+ })
151
+ }
152
+ }
153
+
154
+ fn collect(&mut self) -> Result<(), String> {
155
+ let now = Instant::now();
156
+ let elapsed = self.last_collect.map(|last| now.duration_since(last));
157
+ if let Some(elapsed) = elapsed {
158
+ if elapsed < PDH_MIN_INTERVAL {
159
+ // A burst of requests (overview + page refresh) must not pay the
160
+ // warm-up window again; PDH still holds the last valid sample.
161
+ return Ok(());
162
+ }
163
+ }
164
+ let stale = elapsed.map_or(true, |elapsed| elapsed >= PDH_RESAMPLE_AFTER);
165
+ unsafe {
166
+ if PdhCollectQueryData(self.query) != 0 {
167
+ return Err("PdhCollectQueryData failed".to_string());
168
+ }
169
+ if stale {
170
+ std::thread::sleep(PDH_WARMUP);
171
+ if PdhCollectQueryData(self.query) != 0 {
172
+ return Err("PdhCollectQueryData failed".to_string());
173
+ }
174
+ }
175
+ }
176
+ self.last_collect = Some(Instant::now());
177
+ Ok(())
178
+ }
179
+
180
+ fn formatted_array(&self) -> Result<Vec<(String, f64)>, String> {
181
+ unsafe {
182
+ let mut size = 0_u32;
183
+ let mut count = 0_u32;
184
+ let status = PdhGetFormattedCounterArrayW(
185
+ self.counter,
186
+ PDH_FMT_DOUBLE,
187
+ &mut size,
188
+ &mut count,
189
+ None,
190
+ );
191
+ if status != 0 && status != PDH_MORE_DATA {
192
+ return Err(format!("counter query failed (status {status})"));
193
+ }
194
+ if size == 0 || count == 0 {
195
+ return Ok(Vec::new());
196
+ }
197
+
198
+ // `size` is a byte count; allocate word-aligned storage for the array.
199
+ let words = (size as usize).div_ceil(size_of::<usize>());
200
+ let mut buffer = vec![0_usize; words];
201
+ let items = buffer.as_mut_ptr() as *mut PDH_FMT_COUNTERVALUE_ITEM_W;
202
+ let status = PdhGetFormattedCounterArrayW(
203
+ self.counter,
204
+ PDH_FMT_DOUBLE,
205
+ &mut size,
206
+ &mut count,
207
+ Some(items),
208
+ );
209
+ if status != 0 {
210
+ return Err(format!("counter array failed (status {status})"));
211
+ }
212
+ let slice = std::slice::from_raw_parts(items, count as usize);
213
+ let mut values = Vec::with_capacity(slice.len());
214
+ for item in slice {
215
+ let name = if item.szName.is_null() {
216
+ String::new()
217
+ } else {
218
+ item.szName.to_string().unwrap_or_default()
219
+ };
220
+ values.push((name, item.FmtValue.Anonymous.doubleValue));
221
+ }
222
+ Ok(values)
223
+ }
224
+ }
225
+ }
226
+
227
+ impl Drop for PdhCounter {
228
+ fn drop(&mut self) {
229
+ unsafe {
230
+ let _ = PdhCloseQuery(self.query);
231
+ }
232
+ }
233
+ }
234
+
235
+ enum PdhEntry {
236
+ Ready(PdhCounter),
237
+ /// Cache a missing/failed counter for a while so an unsupported thermal or
238
+ /// GPU counter does not retry its setup on every sample.
239
+ Failed {
240
+ until: Instant,
241
+ error: String,
242
+ },
243
+ }
244
+
245
+ thread_local! {
246
+ static PDH_COUNTERS: RefCell<HashMap<String, PdhEntry>> = RefCell::new(HashMap::new());
247
+ }
248
+
249
+ fn with_pdh_counter<T>(
250
+ path: &str,
251
+ read: impl FnOnce(&PdhCounter) -> Result<T, String>,
252
+ ) -> Result<T, String> {
253
+ PDH_COUNTERS.with(|cell| {
254
+ let mut counters = cell.borrow_mut();
255
+ let now = Instant::now();
256
+ let failure = counters.get(path).and_then(|entry| match entry {
257
+ PdhEntry::Failed { until, error } => Some((*until, error.clone())),
258
+ PdhEntry::Ready(_) => None,
259
+ });
260
+ if let Some((until, error)) = failure {
261
+ if now < until {
262
+ return Err(error);
263
+ }
264
+ counters.remove(path);
265
+ }
266
+ if !counters.contains_key(path) {
267
+ match PdhCounter::open(path) {
268
+ Ok(counter) => {
269
+ counters.insert(path.to_string(), PdhEntry::Ready(counter));
270
+ }
271
+ Err(error) => {
272
+ counters.insert(
273
+ path.to_string(),
274
+ PdhEntry::Failed {
275
+ until: now + Duration::from_secs(60),
276
+ error: error.clone(),
277
+ },
278
+ );
279
+ return Err(error);
280
+ }
281
+ }
282
+ }
283
+
284
+ let collected = match counters.get_mut(path) {
285
+ Some(PdhEntry::Ready(counter)) => counter.collect(),
286
+ _ => unreachable!("PDH counter was inserted above"),
287
+ };
288
+ if let Err(error) = collected {
289
+ counters.insert(
290
+ path.to_string(),
291
+ PdhEntry::Failed {
292
+ until: now + Duration::from_secs(30),
293
+ error: error.clone(),
294
+ },
295
+ );
296
+ return Err(error);
297
+ }
298
+ match counters.get(path) {
299
+ Some(PdhEntry::Ready(counter)) => read(counter),
300
+ _ => unreachable!("PDH counter was collected above"),
301
+ }
302
+ })
303
+ }
304
+
305
+ /// Returns the counter value as a percentage of nominal frequency (100 = base).
306
+ fn pdh_performance_percent() -> Option<f64> {
307
+ with_pdh_counter(PROCESSOR_PERFORMANCE_COUNTER, |counter| {
308
+ let mut value = PDH_FMT_COUNTERVALUE::default();
309
+ let status = unsafe {
310
+ PdhGetFormattedCounterValue(counter.counter, PDH_FMT_DOUBLE, None, &mut value)
311
+ };
312
+ if status != 0 {
313
+ return Err(format!("counter value failed (status {status})"));
314
+ }
315
+ let percent = unsafe { value.Anonymous.doubleValue };
316
+ if percent.is_finite() && percent > 0.0 {
317
+ Ok(Some(percent))
318
+ } else {
319
+ Ok(None)
320
+ }
321
+ })
322
+ .ok()
323
+ .flatten()
324
+ }
325
+
326
+ /// Read every instance of a wildcard PDH counter. The query is reused between
327
+ /// samples; the first read (and any read after an idle gap) takes two samples so
328
+ /// rate counters have a value.
329
+ fn pdh_array(counter_path: &str) -> Result<Vec<(String, f64)>, String> {
330
+ with_pdh_counter(counter_path, PdhCounter::formatted_array)
331
+ }
332
+
333
+ fn is_virtual_adapter(name: &str) -> bool {
334
+ let name = name.to_lowercase();
335
+ [
336
+ "virtual",
337
+ "gameviewer",
338
+ "remote",
339
+ "basic display",
340
+ "microsoft basic",
341
+ ]
342
+ .iter()
343
+ .any(|marker| name.contains(marker))
344
+ }
345
+
346
+ fn wide_string(value: &[u16]) -> String {
347
+ let end = value
348
+ .iter()
349
+ .position(|character| *character == 0)
350
+ .unwrap_or(value.len());
351
+ String::from_utf16_lossy(&value[..end]).trim().to_string()
352
+ }
353
+
354
+ #[derive(Debug, Clone, Default)]
355
+ struct RegistryAdapter {
356
+ name: String,
357
+ driver: String,
358
+ memory_total: Option<u64>,
359
+ }
360
+
361
+ /// Per-adapter performance data from the WDDM kernel-mode interface
362
+ /// (`D3DKMTQueryAdapterInfo`). This is the vendor-neutral source Task Manager
363
+ /// uses for the GPU clock, temperature, power and fan speed.
364
+ #[derive(Debug, Clone, Copy, Default)]
365
+ struct AdapterPerf {
366
+ core_clock_mhz: Option<f64>,
367
+ core_clock_max_mhz: Option<f64>,
368
+ memory_clock_mhz: Option<f64>,
369
+ temperature_celsius: Option<f64>,
370
+ power_percent: Option<f64>,
371
+ fan_rpm: Option<u32>,
372
+ }
373
+
374
+ fn query_adapter_info<T>(h_adapter: u32, kind: KMTQUERYADAPTERINFOTYPE, value: &mut T) -> bool {
375
+ let mut query = D3DKMT_QUERYADAPTERINFO {
376
+ hAdapter: h_adapter,
377
+ Type: kind,
378
+ pPrivateDriverData: value as *mut T as *mut core::ffi::c_void,
379
+ PrivateDriverDataSize: size_of::<T>() as u32,
380
+ };
381
+ unsafe { D3DKMTQueryAdapterInfo(&mut query).0 >= 0 }
382
+ }
383
+
384
+ /// Read WDDM performance data for an adapter identified by its DXGI LUID.
385
+ ///
386
+ /// * `D3DKMT_ADAPTER_PERFDATA` reports memory clock, die temperature, power
387
+ /// (tenths of a percent) and fan RPM.
388
+ /// * `D3DKMT_NODE_PERFDATA` reports per-engine clocks in hertz; the 3D engine is
389
+ /// the graphics clock shown in Task Manager.
390
+ fn adapter_perf(luid: LUID) -> Option<AdapterPerf> {
391
+ unsafe {
392
+ let mut open = D3DKMT_OPENADAPTERFROMLUID {
393
+ AdapterLuid: luid,
394
+ hAdapter: 0,
395
+ };
396
+ if D3DKMTOpenAdapterFromLuid(&mut open).0 < 0 || open.hAdapter == 0 {
397
+ return None;
398
+ }
399
+ let mut perf = AdapterPerf::default();
400
+
401
+ let mut adapter = D3DKMT_ADAPTER_PERFDATA::default();
402
+ if query_adapter_info(open.hAdapter, KMTQAITYPE_ADAPTERPERFDATA, &mut adapter) {
403
+ if adapter.Temperature > 0 {
404
+ let celsius = adapter.Temperature as f64 / 10.0;
405
+ if (5.0..=125.0).contains(&celsius) {
406
+ perf.temperature_celsius = Some(util::round1(celsius));
407
+ }
408
+ }
409
+ if adapter.MemoryFrequency > 0 {
410
+ perf.memory_clock_mhz =
411
+ Some(util::round1(adapter.MemoryFrequency as f64 / 1_000_000.0));
412
+ }
413
+ // The driver reports tenths of a percent; 0 means "unsupported".
414
+ if adapter.Power > 0 && adapter.Power <= 1000 {
415
+ perf.power_percent = Some(util::round1(adapter.Power as f64 / 10.0));
416
+ }
417
+ if adapter.FanRPM > 0 {
418
+ perf.fan_rpm = Some(adapter.FanRPM);
419
+ }
420
+ }
421
+
422
+ // Engine nodes are 1-based in the WDDM metadata (node 0 is a placeholder
423
+ // on some drivers), so probe a generous range and pick the 3D engine.
424
+ let mut fallback_clock: Option<(f64, f64)> = None;
425
+ for node in 0..16u32 {
426
+ let mut metadata = D3DKMT_NODEMETADATA {
427
+ NodeOrdinalAndAdapterIndex: node,
428
+ ..Default::default()
429
+ };
430
+ if !query_adapter_info(open.hAdapter, KMTQAITYPE_NODEMETADATA, &mut metadata) {
431
+ continue;
432
+ }
433
+ let engine = metadata.NodeData.EngineType;
434
+ let mut data = D3DKMT_NODE_PERFDATA {
435
+ NodeOrdinal: node,
436
+ PhysicalAdapterIndex: 0,
437
+ ..Default::default()
438
+ };
439
+ if !query_adapter_info(open.hAdapter, KMTQAITYPE_NODEPERFDATA, &mut data) {
440
+ continue;
441
+ }
442
+ if data.Frequency == 0 && data.MaxFrequency == 0 {
443
+ continue;
444
+ }
445
+ let frequency = data.Frequency as f64 / 1_000_000.0;
446
+ let maximum = data.MaxFrequency as f64 / 1_000_000.0;
447
+ if engine == DXGK_ENGINE_TYPE_3D {
448
+ // An idle GPU may legitimately report 0 MHz on the 3D node;
449
+ // forward it instead of pretending the clock is unavailable.
450
+ perf.core_clock_mhz = Some(util::round1(frequency));
451
+ if maximum > 0.0 {
452
+ perf.core_clock_max_mhz = Some(util::round1(maximum));
453
+ }
454
+ } else if frequency > 0.0 && fallback_clock.map_or(true, |(value, _)| frequency > value)
455
+ {
456
+ fallback_clock = Some((frequency, maximum));
457
+ }
458
+ }
459
+ if perf.core_clock_mhz.is_none() {
460
+ if let Some((frequency, maximum)) = fallback_clock {
461
+ perf.core_clock_mhz = Some(util::round1(frequency));
462
+ if maximum > 0.0 {
463
+ perf.core_clock_max_mhz = Some(util::round1(maximum));
464
+ }
465
+ }
466
+ }
467
+
468
+ let close = D3DKMT_CLOSEADAPTER {
469
+ hAdapter: open.hAdapter,
470
+ };
471
+ let _ = D3DKMTCloseAdapter(&close);
472
+ Some(perf)
473
+ }
474
+ }
475
+
476
+ fn registry_adapters() -> Vec<RegistryAdapter> {
477
+ let Ok(root) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(GPU_CLASS_KEY) else {
478
+ return Vec::new();
479
+ };
480
+ let mut adapters = Vec::new();
481
+ for subkey in root.enum_keys().flatten() {
482
+ let Ok(key) = root.open_subkey(&subkey) else {
483
+ continue;
484
+ };
485
+ let Ok(name) = key.get_value::<String, _>("DriverDesc") else {
486
+ continue;
487
+ };
488
+ let name = name.trim().to_string();
489
+ if name.is_empty() || is_virtual_adapter(&name) {
490
+ continue;
491
+ }
492
+ let driver = key
493
+ .get_value::<String, _>("DriverVersion")
494
+ .ok()
495
+ .map(|value| value.trim().to_string())
496
+ .unwrap_or_default();
497
+ let memory_total = key
498
+ .get_value::<u64, _>("HardwareInformation.qwMemorySize")
499
+ .ok()
500
+ .or_else(|| {
501
+ key.get_raw_value("HardwareInformation.qwMemorySize")
502
+ .ok()
503
+ .and_then(|raw| {
504
+ if raw.bytes.len() < 8 {
505
+ return None;
506
+ }
507
+ let bytes: [u8; 8] = raw.bytes[..8].try_into().ok()?;
508
+ match raw.vtype {
509
+ REG_QWORD | REG_BINARY => Some(u64::from_le_bytes(bytes)),
510
+ _ => None,
511
+ }
512
+ })
513
+ })
514
+ .filter(|value| *value > 0);
515
+ adapters.push(RegistryAdapter {
516
+ name,
517
+ driver,
518
+ memory_total,
519
+ });
520
+ }
521
+ adapters
522
+ }
523
+
524
+ #[derive(Debug, Clone, Default)]
525
+ struct DxgiAdapter {
526
+ name: String,
527
+ memory_total: u64,
528
+ driver: String,
529
+ /// DXGI LUID, used to open the adapter for WDDM performance data.
530
+ luid: Option<LUID>,
531
+ /// Whether the adapter drives at least one display output.
532
+ has_output: bool,
533
+ }
534
+
535
+ fn dxgi_adapters() -> Result<Vec<DxgiAdapter>, String> {
536
+ let registry = registry_adapters();
537
+ unsafe {
538
+ let factory: IDXGIFactory1 =
539
+ CreateDXGIFactory1().map_err(|error| format!("DXGI unavailable: {error}"))?;
540
+ let mut adapters = Vec::new();
541
+ let mut index = 0_u32;
542
+ loop {
543
+ let adapter = match factory.EnumAdapters1(index) {
544
+ Ok(adapter) => adapter,
545
+ Err(_) => break,
546
+ };
547
+ index += 1;
548
+ let Ok(desc) = adapter.GetDesc1() else {
549
+ continue;
550
+ };
551
+ let name = wide_string(&desc.Description);
552
+ if name.is_empty() || is_virtual_adapter(&name) {
553
+ continue;
554
+ }
555
+ let driver = registry
556
+ .iter()
557
+ .find(|candidate| candidate.name.eq_ignore_ascii_case(&name))
558
+ .map(|candidate| candidate.driver.clone())
559
+ .unwrap_or_default();
560
+ adapters.push(DxgiAdapter {
561
+ name,
562
+ memory_total: desc.DedicatedVideoMemory as u64,
563
+ driver,
564
+ luid: Some(desc.AdapterLuid),
565
+ has_output: adapter.EnumOutputs(0).is_ok(),
566
+ });
567
+ }
568
+ Ok(adapters)
569
+ }
570
+ }
571
+
572
+ fn gpu_from_adapters(adapters: &[DxgiAdapter], source: &str) -> Option<Gpu> {
573
+ // Some drivers expose the same physical adapter through more than one DXGI
574
+ // entry; collapse identical descriptions before reporting/aggregating.
575
+ let mut seen = HashSet::new();
576
+ let adapters: Vec<DxgiAdapter> = adapters
577
+ .iter()
578
+ .filter(|adapter| seen.insert(adapter.name.to_lowercase()))
579
+ .cloned()
580
+ .collect();
581
+ if adapters.is_empty() {
582
+ return None;
583
+ }
584
+ // The adapter that drives a display output is the "primary" one and the one
585
+ // Task Manager reports; fall back to the largest VRAM.
586
+ let primary = adapters
587
+ .iter()
588
+ .find(|adapter| adapter.has_output)
589
+ .or_else(|| adapters.iter().max_by_key(|adapter| adapter.memory_total))?;
590
+ let registry = registry_adapters();
591
+ let registry_total = registry
592
+ .iter()
593
+ .filter_map(|adapter| adapter.memory_total)
594
+ .max();
595
+ let memory_total = registry_total.or_else(|| {
596
+ if primary.memory_total > 0 {
597
+ Some(primary.memory_total)
598
+ } else {
599
+ None
600
+ }
601
+ });
602
+ let memory_used = pdh_array(GPU_MEMORY_COUNTER).ok().and_then(|values| {
603
+ values
604
+ .into_iter()
605
+ .map(|(_, value)| value)
606
+ .filter(|value| value.is_finite() && *value > 0.0)
607
+ .reduce(f64::max)
608
+ .map(|value| value.round() as u64)
609
+ });
610
+ let utilization = pdh_array(GPU_ENGINE_COUNTER)
611
+ .ok()
612
+ .map(|values| {
613
+ values
614
+ .into_iter()
615
+ .map(|(_, value)| value)
616
+ .filter(|value| value.is_finite() && *value > 0.0)
617
+ .sum::<f64>()
618
+ })
619
+ .filter(|value| *value > 0.0)
620
+ .map(|value| util::round1(util::clamp(value, 0.0, 100.0)));
621
+ let memory_percent = match (memory_used, memory_total) {
622
+ (Some(used), Some(total)) if total > 0 => {
623
+ Some(util::round1(used as f64 / total as f64 * 100.0))
624
+ }
625
+ _ => None,
626
+ };
627
+ let perf = primary.luid.and_then(adapter_perf);
628
+ Some(Gpu {
629
+ name: primary.name.clone(),
630
+ driver: primary.driver.clone(),
631
+ adapters: adapters
632
+ .iter()
633
+ .map(|adapter| adapter.name.clone())
634
+ .collect(),
635
+ utilization,
636
+ memory_used,
637
+ memory_total,
638
+ memory_percent,
639
+ core_clock_mhz: perf.and_then(|value| value.core_clock_mhz),
640
+ core_clock_max_mhz: perf.and_then(|value| value.core_clock_max_mhz),
641
+ memory_clock_mhz: perf.and_then(|value| value.memory_clock_mhz),
642
+ temperature_celsius: perf.and_then(|value| value.temperature_celsius),
643
+ power_percent: perf.and_then(|value| value.power_percent),
644
+ fan_rpm: perf.and_then(|value| value.fan_rpm),
645
+ ok: true,
646
+ source: source.to_string(),
647
+ error: None,
648
+ })
649
+ }
650
+
651
+ fn read_gpu() -> Option<Gpu> {
652
+ if let Ok(adapters) = dxgi_adapters() {
653
+ if let Some(gpu) = gpu_from_adapters(&adapters, "dxgi-pdh") {
654
+ return Some(gpu);
655
+ }
656
+ }
657
+ let registry = registry_adapters();
658
+ let adapters: Vec<DxgiAdapter> = registry
659
+ .iter()
660
+ .map(|adapter| DxgiAdapter {
661
+ name: adapter.name.clone(),
662
+ memory_total: adapter.memory_total.unwrap_or(0),
663
+ driver: adapter.driver.clone(),
664
+ ..Default::default()
665
+ })
666
+ .collect();
667
+ gpu_from_adapters(&adapters, "registry-pdh")
668
+ }
669
+
670
+ fn thermal_value(raw: f64, high_precision: bool) -> Option<f64> {
671
+ let candidates: [f64; 2] = if high_precision {
672
+ [raw / 10.0 - 273.15, raw - 273.15]
673
+ } else {
674
+ [raw - 273.15, raw / 10.0 - 273.15]
675
+ };
676
+ candidates
677
+ .into_iter()
678
+ .find(|celsius| (5.0..=125.0).contains(celsius))
679
+ }
680
+
681
+ /// `IOCTL_STORAGE_QUERY_PROPERTY` with `StorageDeviceTemperatureProperty`.
682
+ /// Works for NVMe/SATA drives as a standard user (the device is opened with no
683
+ /// access rights: the query itself only needs a handle).
684
+ const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D_1400;
685
+ const STORAGE_DEVICE_TEMPERATURE_PROPERTY: u32 = 51;
686
+ /// `STORAGE_TEMPERATURE_VALUE_NOT_REPORTED` as a signed 16-bit value.
687
+ const TEMPERATURE_NOT_REPORTED: i16 = i16::MIN;
688
+ const MAX_PHYSICAL_DRIVES: u32 = 8;
689
+
690
+ #[repr(C)]
691
+ #[derive(Default)]
692
+ struct StoragePropertyQuery {
693
+ property_id: u32,
694
+ query_type: u32,
695
+ additional: [u8; 1],
696
+ }
697
+
698
+ #[repr(C)]
699
+ #[derive(Debug, Clone, Copy, Default)]
700
+ struct StorageTemperatureInfo {
701
+ index: u16,
702
+ temperature: i16,
703
+ over_threshold: i16,
704
+ under_threshold: i16,
705
+ over_threshold_changeable: u8,
706
+ under_threshold_changeable: u8,
707
+ event_generated: u8,
708
+ reserved0: u8,
709
+ reserved1: u32,
710
+ }
711
+
712
+ #[repr(C)]
713
+ #[derive(Default)]
714
+ struct StorageTemperatureDescriptor {
715
+ version: u32,
716
+ size: u32,
717
+ critical_temperature: i16,
718
+ warning_temperature: i16,
719
+ info_count: u16,
720
+ reserved0: [u8; 2],
721
+ reserved1: [u32; 2],
722
+ temperature_info: [StorageTemperatureInfo; 16],
723
+ }
724
+
725
+ /// The device temperature (sensor index 0 is the composite reading) of one
726
+ /// physical drive, or `None` when the drive does not report it.
727
+ fn disk_temperature(index: u32) -> Option<f64> {
728
+ let path = HSTRING::from(format!(r"\\.\PhysicalDrive{index}"));
729
+ let handle: HANDLE = unsafe {
730
+ CreateFileW(
731
+ &path,
732
+ 0,
733
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
734
+ None,
735
+ OPEN_EXISTING,
736
+ FILE_ATTRIBUTE_NORMAL,
737
+ None,
738
+ )
739
+ }
740
+ .ok()?;
741
+ if handle.is_invalid() {
742
+ return None;
743
+ }
744
+ let mut query = StoragePropertyQuery {
745
+ property_id: STORAGE_DEVICE_TEMPERATURE_PROPERTY,
746
+ query_type: 0,
747
+ additional: [0],
748
+ };
749
+ let mut output = StorageTemperatureDescriptor::default();
750
+ let mut returned = 0_u32;
751
+ let ok = unsafe {
752
+ DeviceIoControl(
753
+ handle,
754
+ IOCTL_STORAGE_QUERY_PROPERTY,
755
+ Some(&mut query as *mut _ as *mut core::ffi::c_void),
756
+ size_of::<StoragePropertyQuery>() as u32,
757
+ Some(&mut output as *mut _ as *mut core::ffi::c_void),
758
+ size_of::<StorageTemperatureDescriptor>() as u32,
759
+ Some(&mut returned),
760
+ None,
761
+ )
762
+ };
763
+ unsafe {
764
+ let _ = CloseHandle(handle);
765
+ }
766
+ if ok.is_err() {
767
+ return None;
768
+ }
769
+ let count = (output.info_count as usize).min(output.temperature_info.len());
770
+ let mut fallback = None;
771
+ for info in output.temperature_info.iter().take(count) {
772
+ let value = info.temperature;
773
+ if value == TEMPERATURE_NOT_REPORTED || value <= 0 || value > 125 {
774
+ continue;
775
+ }
776
+ // Sensor 0 is the composite (drive-level) reading; prefer it.
777
+ if info.index == 0 {
778
+ return Some(util::round1(value as f64));
779
+ }
780
+ fallback = Some(fallback.map_or(value, |current: i16| current.max(value)));
781
+ }
782
+ fallback.map(|value| util::round1(value as f64))
783
+ }
784
+
785
+ fn disk_temperatures() -> Vec<(String, f64)> {
786
+ (0..MAX_PHYSICAL_DRIVES)
787
+ .filter_map(|index| {
788
+ disk_temperature(index).map(|celsius| (format!("磁盘 {index}"), celsius))
789
+ })
790
+ .collect()
791
+ }
792
+
793
+ fn is_cpu_sensor_name(name: &str) -> bool {
794
+ let lower = name.to_lowercase();
795
+ ["cpu", "core", "package", "processor", "tdie", "tctl"]
796
+ .iter()
797
+ .any(|marker| lower.contains(marker))
798
+ }
799
+
800
+ fn read_temperatures(gpu: Option<&Gpu>) -> Vec<Temperature> {
801
+ let mut temperatures = Vec::new();
802
+ let mut seen = HashSet::new();
803
+ // The WDDM die temperature is the reading Task Manager shows under GPU.
804
+ if let Some(gpu) = gpu {
805
+ if let Some(celsius) = gpu.temperature_celsius {
806
+ seen.insert(gpu.name.to_lowercase());
807
+ temperatures.push(Temperature {
808
+ name: gpu.name.clone(),
809
+ celsius,
810
+ });
811
+ }
812
+ }
813
+ for (name, celsius) in disk_temperatures() {
814
+ if !seen.insert(name.to_lowercase()) {
815
+ continue;
816
+ }
817
+ temperatures.push(Temperature { name, celsius });
818
+ }
819
+
820
+ let mut zones: Vec<Temperature> = Vec::new();
821
+ for (counter, high_precision) in [(THERMAL_PRECISION_COUNTER, true), (THERMAL_COUNTER, false)] {
822
+ let Ok(values) = pdh_array(counter) else {
823
+ continue;
824
+ };
825
+ let mut zone_seen = HashSet::new();
826
+ for (name, raw) in values {
827
+ if !raw.is_finite() {
828
+ continue;
829
+ }
830
+ let Some(celsius) = thermal_value(raw, high_precision) else {
831
+ continue;
832
+ };
833
+ let name = if name.is_empty() {
834
+ "Thermal Zone".to_string()
835
+ } else {
836
+ name
837
+ };
838
+ if !zone_seen.insert(name.to_lowercase()) {
839
+ continue;
840
+ }
841
+ zones.push(Temperature {
842
+ name,
843
+ celsius: util::round1(celsius),
844
+ });
845
+ }
846
+ if !zones.is_empty() {
847
+ break;
848
+ }
849
+ }
850
+ // ACPI thermal zones rarely carry a friendly name. The hottest zone is the
851
+ // CPU package on virtually every laptop/board that exposes zones, so label
852
+ // it explicitly instead of showing an opaque `\_TZ.TZ00`.
853
+ if !zones.is_empty() && !zones.iter().any(|zone| is_cpu_sensor_name(&zone.name)) {
854
+ if let Some(hottest) = zones
855
+ .iter_mut()
856
+ .max_by(|left, right| left.celsius.total_cmp(&right.celsius))
857
+ {
858
+ hottest.name = "CPU".to_string();
859
+ }
860
+ }
861
+ for zone in zones {
862
+ if seen.insert(zone.name.to_lowercase()) {
863
+ temperatures.push(zone);
864
+ }
865
+ }
866
+ temperatures
867
+ }
868
+
869
+ pub fn read() -> Result<Performance, String> {
870
+ let (mut cpu, memory) = common::sample_system();
871
+ let (model, registry_base) = registry_cpu();
872
+ if let Some(name) = model {
873
+ cpu.model = name;
874
+ }
875
+ if cpu.base_mhz.is_none() {
876
+ cpu.base_mhz = registry_base;
877
+ }
878
+
879
+ // Prefer the PDH percentage (matches the old probe and reacts to boost);
880
+ // fall back to CallNtPowerInformation on machines without the counter.
881
+ let mut speed = None;
882
+ if let (Some(base), Some(percent)) = (cpu.base_mhz, pdh_performance_percent()) {
883
+ let mut current = base * percent / 100.0;
884
+ if current > base * 2.0 {
885
+ current = base * 2.0;
886
+ }
887
+ if current > 0.0 {
888
+ speed = Some(util::round1(current));
889
+ }
890
+ }
891
+ if speed.is_none() {
892
+ if let Some((current, max)) = power_frequency() {
893
+ if current > 0.0 {
894
+ speed = Some(current);
895
+ }
896
+ if cpu.base_mhz.is_none() && max > 0.0 {
897
+ cpu.base_mhz = Some(max);
898
+ }
899
+ }
900
+ }
901
+ cpu.speed_mhz = speed;
902
+
903
+ let gpu = read_gpu();
904
+ let temperatures = read_temperatures(gpu.as_ref());
905
+ let mut performance = Performance::success(cpu, memory);
906
+ performance.gpu = gpu;
907
+ performance.temperatures = temperatures;
908
+ Ok(performance)
909
+ }
910
+
911
+ #[cfg(test)]
912
+ mod tests {
913
+ use super::*;
914
+
915
+ #[test]
916
+ fn registry_name_is_readable() {
917
+ if let (Some(name), _) = registry_cpu() {
918
+ assert!(!name.trim().is_empty());
919
+ }
920
+ }
921
+
922
+ #[test]
923
+ fn pdh_counter_is_available() {
924
+ if let Some(percent) = pdh_performance_percent() {
925
+ assert!(percent > 0.0);
926
+ }
927
+ }
928
+
929
+ #[test]
930
+ fn filters_virtual_adapters() {
931
+ assert!(is_virtual_adapter("Microsoft Basic Display Adapter"));
932
+ assert!(is_virtual_adapter("GameViewer Virtual Display"));
933
+ assert!(!is_virtual_adapter("NVIDIA GeForce RTX 4070"));
934
+ }
935
+
936
+ #[test]
937
+ fn converts_thermal_kelvin_values() {
938
+ let high = thermal_value(3001.5, true).unwrap();
939
+ assert!((high - 27.0).abs() < 0.2);
940
+ let normal_tenths = thermal_value(300.15, false).unwrap();
941
+ assert!((normal_tenths - 27.0).abs() < 0.2);
942
+ let normal_kelvin = thermal_value(300.15, true).unwrap();
943
+ assert!((normal_kelvin - 27.0).abs() < 0.2);
944
+ assert_eq!(thermal_value(10.0, false), None);
945
+ }
946
+
947
+ #[test]
948
+ fn gpu_reading_has_a_plausible_shape() {
949
+ if let Some(gpu) = read_gpu() {
950
+ assert!(gpu.ok);
951
+ assert!(!gpu.name.is_empty());
952
+ if let Some(value) = gpu.utilization {
953
+ assert!((0.0..=100.0).contains(&value));
954
+ }
955
+ }
956
+ }
957
+
958
+ #[test]
959
+ fn gpu_perf_values_are_plausible() {
960
+ if let Some(gpu) = read_gpu() {
961
+ if let Some(value) = gpu.core_clock_mhz {
962
+ assert!((0.0..10_000.0).contains(&value), "core clock {value}");
963
+ }
964
+ if let Some(value) = gpu.core_clock_max_mhz {
965
+ assert!(value > 0.0 && value < 10_000.0, "max clock {value}");
966
+ }
967
+ if let Some(value) = gpu.memory_clock_mhz {
968
+ assert!(value > 0.0 && value < 30_000.0, "memory clock {value}");
969
+ }
970
+ if let Some(value) = gpu.temperature_celsius {
971
+ assert!((5.0..=125.0).contains(&value), "temperature {value}");
972
+ }
973
+ if let Some(value) = gpu.power_percent {
974
+ assert!((0.0..=100.0).contains(&value), "power {value}");
975
+ }
976
+ if let Some(value) = gpu.fan_rpm {
977
+ assert!(value < 20_000, "fan {value}");
978
+ }
979
+ }
980
+ }
981
+ }