@windypro-rourou/dsh-logcat 0.5.0-preview.1 → 0.5.0-preview.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/lib/client.js CHANGED
@@ -33,51 +33,64 @@ window.__ModuleLoader__.load({
33
33
  [data-dsh-logcat-entry][data-active] { background: rgba(128,128,128,.22); }
34
34
  [data-dsh-logcat-entry] .lc-entry-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; flex: none; }
35
35
  [data-dsh-logcat-entry] .lc-entry-label { font-size: 13px; line-height: 1.2; opacity: .92; }
36
- .dsh-logcat-view { position: fixed; top: 0; right: 0; bottom: 0; width: min(620px, 94vw); display: flex; flex-direction: column; background: var(--lc-bg, #ffffff); border-left: 1px solid rgba(128,128,128,.3); box-shadow: -10px 0 28px rgba(0,0,0,.18); z-index: 9999; }
36
+ .dsh-logcat-view { position: fixed; top: 0; right: 0; bottom: 0; width: min(620px, 94vw); z-index: 9999; }
37
37
  .dsh-logcat-view[hidden] { display: none !important; }
38
+ .dsh-logcat-panel {
39
+ position: absolute; inset: 0; display: flex; flex-direction: column;
40
+ background: light-dark(#ffffff, #171a1f);
41
+ color: light-dark(#1a1d23, #dfe3ea);
42
+ border-left: 1px solid light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14));
43
+ box-shadow: -10px 0 28px rgba(0,0,0,.22);
44
+ }
38
45
  .dsh-logcat-resize { position: absolute; left: -6px; top: 0; bottom: 0; width: 12px; cursor: col-resize; z-index: 2; display: flex; align-items: center; justify-content: center; }
39
- .dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: rgba(128,128,128,.35); transition: background .15s, height .15s; }
40
- .dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: rgba(128,128,128,.75); height: 72px; }
41
- .dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: rgba(128,128,128,.16); }
46
+ .dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: light-dark(rgba(0,0,0,.3), rgba(255,255,255,.35)); transition: background .15s, height .15s; }
47
+ .dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: light-dark(rgba(0,0,0,.6), rgba(255,255,255,.75)); height: 72px; }
48
+ .dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.14)); }
42
49
  .lc-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
43
- .lc-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid rgba(128,128,128,.25); flex: none; }
50
+ .lc-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; }
44
51
  .lc-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; display: flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 6px; font-size: 13px; }
45
- .lc-back:hover { background: rgba(128,128,128,.14); }
46
- .lc-title { font-size: 15px; font-weight: 600; margin: 0; }
52
+ .lc-back:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
53
+ .lc-title { font-size: 15px; font-weight: 700; margin: 0; letter-spacing: .2px; }
47
54
  .lc-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
48
- .lc-dot.on { background: #4caf50; }
55
+ .lc-dot.on { background: #4caf50; box-shadow: 0 0 6px rgba(76,175,80,.6); }
49
56
  .lc-dot.off { background: #9e9e9e; }
50
57
  .lc-dot.warn { background: #ff9800; }
51
- .lc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid rgba(128,128,128,.25); flex: none; flex-wrap: wrap; }
52
- .lc-select, .lc-search { background: rgba(128,128,128,.08); border: 1px solid rgba(128,128,128,.3); border-radius: 6px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
53
- .lc-select:focus, .lc-search:focus { outline: 1px solid rgba(128,128,128,.5); }
58
+ .lc-stats { display: flex; align-items: center; gap: 6px; padding: 6px 14px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); flex: none; flex-wrap: wrap; font-size: 11px; }
59
+ .lc-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 999px; background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); color: light-dark(#4a4f58, #b9bec7); white-space: nowrap; }
60
+ .lc-chip b { color: inherit; font-weight: 600; }
61
+ .lc-mem-bar { display: inline-block; width: 44px; height: 5px; border-radius: 3px; background: light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14)); overflow: hidden; vertical-align: middle; }
62
+ .lc-mem-fill { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, #4caf50, #fbc02d); }
63
+ .lc-mem-fill.hot { background: linear-gradient(90deg, #fbc02d, #ef5350); }
64
+ .lc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; flex-wrap: wrap; }
65
+ .lc-select, .lc-search { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 5px 9px; }
66
+ .lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
54
67
  .lc-search { flex: 1; min-width: 120px; max-width: 320px; }
55
- .lc-levels { display: flex; gap: 2px; }
56
- .lc-level { border: 1px solid transparent; background: transparent; color: inherit; font: inherit; font-size: 12px; font-weight: 600; width: 24px; height: 24px; border-radius: 5px; cursor: pointer; }
57
- .lc-level:hover { background: rgba(128,128,128,.12); }
58
- .lc-level[data-on] { background: rgba(128,128,128,.22); border-color: rgba(128,128,128,.4); }
68
+ .lc-levels { display: flex; gap: 2px; background: light-dark(rgba(0,0,0,.04), rgba(255,255,255,.06)); padding: 2px; border-radius: 7px; }
69
+ .lc-level { border: 0; background: transparent; color: inherit; font: inherit; font-size: 12px; font-weight: 600; width: 24px; height: 22px; border-radius: 5px; cursor: pointer; }
70
+ .lc-level:hover { background: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); }
71
+ .lc-level[data-on] { background: light-dark(#fff, #2a2f37); box-shadow: 0 1px 3px rgba(0,0,0,.2); }
59
72
  .lc-level.v { color: #9e9e9e; } .lc-level.d { color: #4fc3f7; } .lc-level.i { color: #4caf50; }
60
73
  .lc-level.w { color: #fbc02d; } .lc-level.e { color: #ef5350; } .lc-level.f { color: #ab47bc; }
61
- .lc-btn { border: 1px solid rgba(128,128,128,.3); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 6px; cursor: pointer; }
62
- .lc-btn:hover { background: rgba(128,128,128,.12); }
63
- .lc-btn[data-on] { background: rgba(128,128,128,.22); }
74
+ .lc-btn { border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 5px 10px; border-radius: 7px; cursor: pointer; transition: background .12s; }
75
+ .lc-btn:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
76
+ .lc-btn[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); border-color: rgba(66,133,244,.45); color: #4285f4; }
64
77
  .lc-body { flex: 1; min-height: 0; position: relative; overflow: hidden; }
65
78
  .lc-log { position: absolute; inset: 0; overflow: auto; font-family: Consolas, "Cascadia Mono", "Courier New", monospace; font-size: 12px; line-height: 20px; }
66
79
  .lc-log-inner { position: relative; }
67
80
  .lc-line { position: absolute; left: 0; right: 0; padding: 0 12px; white-space: pre; overflow: hidden; text-overflow: ellipsis; cursor: default; }
68
- .lc-line:hover { background: rgba(128,128,128,.12); }
69
- .lc-line .ts { color: var(--lc-dim, #9e9e9e); margin-right: 8px; }
70
- .lc-line .pid { color: var(--lc-dim, #9e9e9e); margin-right: 6px; }
81
+ .lc-line:hover { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.07)); }
82
+ .lc-line .ts { color: light-dark(#9aa0a8, #7c838d); margin-right: 8px; }
83
+ .lc-line .pid { color: light-dark(#9aa0a8, #7c838d); margin-right: 6px; }
71
84
  .lc-line .lv { display: inline-block; width: 14px; text-align: center; font-weight: 700; margin-right: 6px; }
72
85
  .lc-line .lv.V { color: #9e9e9e; } .lc-line .lv.D { color: #4fc3f7; } .lc-line .lv.I { color: #4caf50; }
73
86
  .lc-line .lv.W { color: #fbc02d; } .lc-line .lv.E { color: #ef5350; } .lc-line .lv.F { color: #ab47bc; }
74
87
  .lc-line .tag { color: #29b6f6; margin-right: 8px; }
75
- .lc-line.cont .msg { padding-left: 44px; color: var(--lc-dim, #9e9e9e); }
88
+ .lc-line.cont .msg { padding-left: 44px; color: light-dark(#9aa0a8, #7c838d); }
76
89
  .lc-line.crash { background: rgba(239,83,80,.14); box-shadow: inset 2px 0 0 #ef5350; }
77
90
  .lc-line.crash .tag { color: #ef5350; font-weight: 700; }
78
91
  .lc-line.crash .msg { color: #ef5350; }
79
- .lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--lc-dim, #9e9e9e); font-size: 13px; }
80
- .lc-status { display: flex; align-items: center; gap: 14px; padding: 4px 12px; border-top: 1px solid rgba(128,128,128,.25); flex: none; font-size: 11px; color: var(--lc-dim, #9e9e9e); }
92
+ .lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: light-dark(#9aa0a8, #7c838d); font-size: 13px; }
93
+ .lc-status { display: flex; align-items: center; gap: 14px; padding: 5px 14px; border-top: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; font-size: 11px; color: light-dark(#6b7078, #9aa0a8); }
81
94
  .lc-status b { font-weight: 600; color: inherit; }
82
95
  `;
83
96
  //#endregion
@@ -121,6 +134,7 @@ window.__ModuleLoader__.load({
121
134
  const [updateAvailable, setUpdateAvailable] = useState(false);
122
135
  const [updateKind, setUpdateKind] = useState("");
123
136
  const [updateHint, setUpdateHint] = useState("");
137
+ const [stats, setStats] = useState(null);
124
138
  const [level, setLevel] = useState("");
125
139
  const [keyword, setKeyword] = useState("");
126
140
  const [autoScroll, setAutoScroll] = useState(true);
@@ -270,6 +284,34 @@ window.__ModuleLoader__.load({
270
284
  .catch(() => { /* host not up yet */ });
271
285
  }, []);
272
286
 
287
+ // Live device stats (model / memory / cpu / battery) — refresh every 5s.
288
+ useEffect(() => {
289
+ let alive = true;
290
+ let timer = null;
291
+ const load = () => {
292
+ const target = serialRef.current;
293
+ if (target === "") { setStats(null); return; }
294
+ fetch(API_BASE + "/stats?serial=" + encodeURIComponent(target))
295
+ .then((res) => res.json())
296
+ .then((body) => { if (alive) setStats(body?.ready === true ? body : null); })
297
+ .catch(() => { /* device offline */ });
298
+ };
299
+ load();
300
+ timer = setInterval(load, 5000);
301
+ return () => { alive = false; if (timer !== null) clearInterval(timer); };
302
+ }, [serial]);
303
+
304
+ const parseCpu = (line) => {
305
+ if (!line) return undefined;
306
+ const m = /([\d.]+)%cpu\s+.*?\s+([\d.]+)%idle/.exec(line);
307
+ if (m === null) return undefined;
308
+ const total = Number(m[1]);
309
+ if (total <= 0) return undefined;
310
+ return Math.round(((total - Number(m[2])) / total) * 100);
311
+ };
312
+ const BATTERY_STATUS = { 1: "未知", 2: "充电中", 3: "放电", 4: "未充电", 5: "已满" };
313
+ const mb = (kb) => kb !== undefined ? (kb / 1024 / 1024).toFixed(1) : undefined;
314
+
273
315
  const filtered = useMemo(() => {
274
316
  const needle = keyword.trim().toLowerCase();
275
317
  return entries.filter((e) => {
@@ -391,6 +433,33 @@ window.__ModuleLoader__.load({
391
433
  h("option", { key: d.serial, value: d.serial },
392
434
  (d.model !== "" ? d.model + " · " : "") + d.serial + " [" + d.state + "]"))),
393
435
  ),
436
+ h("div", { className: "lc-stats" },
437
+ stats?.model
438
+ ? h("span", { className: "lc-chip", title: "设备型号 / 系统版本" },
439
+ stats.model + (stats.androidVersion ? " · Android " + stats.androidVersion : ""))
440
+ : null,
441
+ stats?.resolution
442
+ ? h("span", { className: "lc-chip", title: "屏幕分辨率" }, stats.resolution)
443
+ : null,
444
+ stats?.memPct !== undefined
445
+ ? h("span", { className: "lc-chip", title: "内存 " + (mb(stats.usedMemKb) ?? "?") + "G / " + (mb(stats.totalMemKb) ?? "?") + "G" },
446
+ "内存 ",
447
+ h("span", { className: "lc-mem-bar" },
448
+ h("span", {
449
+ className: "lc-mem-fill" + (stats.memPct > 85 ? " hot" : ""),
450
+ style: { width: stats.memPct + "%" },
451
+ })),
452
+ " " + stats.memPct + "%")
453
+ : null,
454
+ parseCpu(stats?.cpu) !== undefined
455
+ ? h("span", { className: "lc-chip", title: "CPU 使用率(采样)" }, "CPU " + parseCpu(stats.cpu) + "%")
456
+ : null,
457
+ stats?.batteryLevel !== undefined
458
+ ? h("span", { className: "lc-chip", title: "电量" },
459
+ "电量 " + stats.batteryLevel + "%" + (BATTERY_STATUS[stats.batteryStatus] !== undefined ? " · " + BATTERY_STATUS[stats.batteryStatus] : ""))
460
+ : null,
461
+ stats === null && serial !== "" ? h("span", { className: "lc-chip" }, "正在采集设备状态…") : null,
462
+ ),
394
463
  h("div", { className: "lc-toolbar" },
395
464
  h("div", { className: "lc-levels", role: "group", "aria-label": "日志级别" },
396
465
  h("button", {
@@ -689,6 +758,8 @@ window.__ModuleLoader__.load({
689
758
  root = undefined;
690
759
  container?.remove();
691
760
  handle = undefined;
761
+ // Wrapper keeps the resize handle OUTSIDE the React root — createRoot clears
762
+ // the container's children on render, which used to delete the handle.
692
763
  container = document.createElement("div");
693
764
  container.dataset.dshLogcatView = "";
694
765
  container.className = "dsh-logcat-view";
@@ -699,8 +770,11 @@ window.__ModuleLoader__.load({
699
770
  handle.title = "拖拽调整宽度(双击重置)";
700
771
  handle.addEventListener("mousedown", onDown);
701
772
  container.appendChild(handle);
773
+ const panel = document.createElement("div");
774
+ panel.className = "dsh-logcat-panel";
775
+ container.appendChild(panel);
702
776
  document.body.appendChild(container);
703
- root = createRoot(container);
777
+ root = createRoot(panel);
704
778
  root.render(h(LogcatPanel, { controller }));
705
779
  };
706
780
 
package/lib/index.js CHANGED
@@ -543,6 +543,36 @@ function writeJson(res, status, body) {
543
543
  /** The one WebSocket server for logcat fan-out. */
544
544
  const streamWss = new WebSocketServer({ noServer: true })
545
545
 
546
+ /** One-shot device info + resource stats for the panel (single adb round-trip). */
547
+ async function collectStats(engine, serial) {
548
+ const sh = 'getprop ro.product.model; getprop ro.product.brand; getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.product.cpu.abi; wm size; grep -E "MemTotal|MemFree" /proc/meminfo; dumpsys battery | grep -E "level:|status:"; top -b -n 1 | head -3'
549
+ const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
550
+ const lines = (result.stdout ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
551
+ const get = (i) => lines[i] ?? ''
552
+ const field = (prefix) => { const l = lines.find((x) => x.startsWith(prefix)); return l ? l.slice(prefix.length).trim() : undefined }
553
+ const kb = (prefix) => { const v = field(prefix); return v ? Number.parseInt(v, 10) || undefined : undefined }
554
+ const totalMemKb = kb('MemTotal') ?? 0
555
+ const freeMemKb = kb('MemFree') ?? 0
556
+ const usedMemKb = totalMemKb > 0 ? totalMemKb - freeMemKb : 0
557
+ return {
558
+ ready: true,
559
+ serial,
560
+ model: get(0) || undefined,
561
+ brand: get(1) || undefined,
562
+ androidVersion: get(2) || undefined,
563
+ sdk: get(3) || undefined,
564
+ abi: get(4) || undefined,
565
+ resolution: (lines.find((l) => l.startsWith('Physical size')) ?? '').replace('Physical size: ', '') || undefined,
566
+ totalMemKb,
567
+ freeMemKb,
568
+ usedMemKb,
569
+ memPct: totalMemKb > 0 ? Math.round((usedMemKb / totalMemKb) * 100) : undefined,
570
+ batteryLevel: kb('level'),
571
+ batteryStatus: field('status'),
572
+ cpu: lines.find((l) => /%cpu|Cpu\(s\)|CPU:/i.test(l)) ?? undefined,
573
+ }
574
+ }
575
+
546
576
  /** Build every /api/dsh-logcat route plus the stream upgrade. */
547
577
  function makeRoutes(engine) {
548
578
  const routes = [
@@ -636,6 +666,26 @@ function makeRoutes(engine) {
636
666
  writeJson(res, 200, { package: pkg })
637
667
  },
638
668
  },
669
+ {
670
+ kind: 'exact',
671
+ path: API_BASE + '/stats',
672
+ handler: async (req, res) => {
673
+ if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
674
+ if ((req.method ?? 'GET') !== 'GET') { writeJson(res, 405, { error: 'method not allowed' }); return }
675
+ if (engine.adb === null) { writeJson(res, 200, { ready: false }); return }
676
+ const url = new URL(req.url ?? '/', 'http://localhost')
677
+ const requested = url.searchParams.get('serial') ?? ''
678
+ const serial = requested !== '' && engine.devices.has(requested)
679
+ ? requested
680
+ : [...engine.devices.keys()].find((s) => engine.devices.get(s).state === 'device') ?? ''
681
+ if (serial === '') { writeJson(res, 200, { ready: false }); return }
682
+ try {
683
+ writeJson(res, 200, await collectStats(engine, serial))
684
+ } catch (error) {
685
+ writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
686
+ }
687
+ },
688
+ },
639
689
  {
640
690
  kind: 'exact',
641
691
  path: API_BASE + '/screenshot',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@windypro-rourou/dsh-logcat",
3
3
  "description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
4
- "version": "0.5.0-preview.1",
4
+ "version": "0.5.0-preview.3",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {