@yuuki824/kanshi 0.3.0

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 (45) hide show
  1. package/.env.example +34 -0
  2. package/Dockerfile +53 -0
  3. package/LICENSE +661 -0
  4. package/README.md +219 -0
  5. package/bin/kanshi.js +63 -0
  6. package/docker-compose.yml +84 -0
  7. package/go.mod +3 -0
  8. package/internal/access/access.go +275 -0
  9. package/internal/access/listeners.go +229 -0
  10. package/internal/config/config.go +256 -0
  11. package/internal/config/defaults_other.go +5 -0
  12. package/internal/config/defaults_windows.go +4 -0
  13. package/internal/config/envfile.go +147 -0
  14. package/internal/dockerstats/dial.go +32 -0
  15. package/internal/dockerstats/dial_other.go +13 -0
  16. package/internal/dockerstats/dial_windows.go +62 -0
  17. package/internal/dockerstats/dockerstats.go +626 -0
  18. package/internal/roots/roots.go +175 -0
  19. package/internal/roots/roots_linux.go +132 -0
  20. package/internal/roots/roots_windows.go +74 -0
  21. package/internal/server/access.go +155 -0
  22. package/internal/server/server.go +523 -0
  23. package/internal/server/static.go +165 -0
  24. package/internal/storage/cache.go +283 -0
  25. package/internal/storage/dir_linux.go +177 -0
  26. package/internal/storage/dir_windows.go +244 -0
  27. package/internal/storage/statat_fast.go +21 -0
  28. package/internal/storage/statat_other.go +11 -0
  29. package/internal/storage/storage.go +598 -0
  30. package/internal/storage/sysnum_amd64.go +7 -0
  31. package/internal/storage/sysnum_generic.go +7 -0
  32. package/internal/storage/walk.go +342 -0
  33. package/internal/vitals/vitals.go +246 -0
  34. package/internal/vitals/vitals_linux.go +548 -0
  35. package/internal/vitals/vitals_windows.go +286 -0
  36. package/main.go +129 -0
  37. package/main_other.go +11 -0
  38. package/main_windows.go +29 -0
  39. package/package.json +49 -0
  40. package/web/app.js +835 -0
  41. package/web/apple-touch-icon.png +0 -0
  42. package/web/icon.svg +6 -0
  43. package/web/index.html +151 -0
  44. package/web/style.css +356 -0
  45. package/web/treemap.js +217 -0
package/web/treemap.js ADDED
@@ -0,0 +1,217 @@
1
+ /* Squarified treemap, ~150 lines of SVG. No charting library: the whole point
2
+ * of this app is to have no external dependencies.
3
+ *
4
+ * Bruls/Huizing/van Wijk squarified layout — greedily fills rows along the
5
+ * short side, keeping each tile as close to square as it can, so tiles stay
6
+ * tappable on a phone instead of degenerating into slivers. */
7
+ (function (global) {
8
+ "use strict";
9
+
10
+ const NS = "http://www.w3.org/2000/svg";
11
+ const GAP = 2; // 2px surface gap between fills — never a stroke border
12
+ const RADIUS = 3;
13
+ const GLYPH = { dir: "▸", file: "·", rest: "⋯" };
14
+
15
+ function worstRatio(row, rowSum, shortSide, scale) {
16
+ if (!row.length) return Infinity;
17
+ const s = rowSum * scale;
18
+ if (s <= 0) return Infinity;
19
+ const max = row[0].size * scale;
20
+ const min = row[row.length - 1].size * scale;
21
+ const sq = shortSide * shortSide;
22
+ return Math.max((sq * max) / (s * s), (s * s) / (sq * min));
23
+ }
24
+
25
+ function squarify(items, x, y, w, h) {
26
+ const out = [];
27
+ const remaining = items.filter((d) => d.size > 0).sort((a, b) => b.size - a.size);
28
+ let totalRem = 0;
29
+ for (const item of remaining) totalRem += item.size;
30
+ let next = 0;
31
+
32
+ while (next < remaining.length && w > 0.5 && h > 0.5 && totalRem > 0) {
33
+ const scale = (w * h) / totalRem;
34
+ const shortSide = Math.min(w, h);
35
+ const row = [];
36
+ let rowSum = 0;
37
+ let prevWorst = Infinity;
38
+
39
+ while (next < remaining.length) {
40
+ const candidate = remaining[next];
41
+ row.push(candidate);
42
+ const ratio = worstRatio(row, rowSum + candidate.size, shortSide, scale);
43
+ // Adding this tile is only worth it while it makes the row *less* oblong.
44
+ if (row.length === 1 || ratio <= prevWorst) {
45
+ rowSum += candidate.size;
46
+ prevWorst = ratio;
47
+ next++;
48
+ } else {
49
+ row.pop();
50
+ break;
51
+ }
52
+ }
53
+ totalRem -= rowSum;
54
+
55
+ const rowArea = rowSum * scale;
56
+ if (w >= h) {
57
+ const rw = rowArea / h;
58
+ let cy = y;
59
+ for (const item of row) {
60
+ const rh = (item.size * scale) / rw;
61
+ out.push({ item: item, x: x, y: cy, w: rw, h: rh });
62
+ cy += rh;
63
+ }
64
+ x += rw; w -= rw;
65
+ } else {
66
+ const rh = rowArea / w;
67
+ let cx = x;
68
+ for (const item of row) {
69
+ const rw = (item.size * scale) / rh;
70
+ out.push({ item: item, x: cx, y: y, w: rw, h: rh });
71
+ cx += rw;
72
+ }
73
+ y += rh; h -= rh;
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ function el(name, attrs) {
80
+ const node = document.createElementNS(NS, name);
81
+ for (const key in attrs) node.setAttribute(key, attrs[key]);
82
+ return node;
83
+ }
84
+
85
+ /* Trim a label until it truly fits its tile. The character estimate below is
86
+ * only an estimate — proportional type makes "WWW" three times the width of
87
+ * "iii" — so anything still over budget is measured and cut for real. The
88
+ * node has to be in the document before getComputedTextLength() means
89
+ * anything, hence the second pass after every tile is placed. */
90
+ function fitLabel(entry) {
91
+ const node = entry.node;
92
+ if (node.getComputedTextLength() <= entry.room) return;
93
+ // One proportional guess lands within a character or two; the loop closes
94
+ // the rest. Both run on a handful of labels, never the whole tree.
95
+ let chars = Math.max(1, Math.floor(entry.name.length * (entry.room / node.getComputedTextLength())) - 1);
96
+ node.textContent = entry.prefix + entry.name.slice(0, chars) + "…";
97
+ while (chars > 1 && node.getComputedTextLength() > entry.room) {
98
+ chars -= 1;
99
+ node.textContent = entry.prefix + entry.name.slice(0, chars) + "…";
100
+ }
101
+ // Not even one character plus the ellipsis fits: drop the label rather than
102
+ // let it bleed over the tile edge. The table below still carries the name.
103
+ if (node.getComputedTextLength() > entry.room) node.textContent = "";
104
+ }
105
+
106
+ /* One set of listeners on the <svg>, reading the tile's index back off the
107
+ * event target — rather than four closures per tile rebuilt on every draw. */
108
+ function bind(svg) {
109
+ if (svg.__tm) return;
110
+ svg.__tm = { items: [], opts: {} };
111
+ const pick = (ev) => {
112
+ const tile = ev.target.closest && ev.target.closest(".tile");
113
+ return tile ? svg.__tm.items[+tile.dataset.i] : null;
114
+ };
115
+ const focus = (ev) => {
116
+ const d = pick(ev);
117
+ if (d && svg.__tm.opts.onFocus) svg.__tm.opts.onFocus(d);
118
+ };
119
+ const activate = (ev) => {
120
+ const d = pick(ev);
121
+ if (!d) return;
122
+ ev.preventDefault();
123
+ if (svg.__tm.opts.onSelect) svg.__tm.opts.onSelect(d);
124
+ };
125
+ svg.addEventListener("click", activate);
126
+ svg.addEventListener("mouseover", focus);
127
+ svg.addEventListener("focusin", focus);
128
+ svg.addEventListener("keydown", (ev) => {
129
+ if (ev.key === "Enter" || ev.key === " ") activate(ev);
130
+ });
131
+ }
132
+
133
+ /* render(svg, children, { width, height, fmt, onSelect, onFocus }) */
134
+ function render(svg, children, opts) {
135
+ const W = opts.width, H = opts.height;
136
+ bind(svg);
137
+ svg.__tm.opts = opts;
138
+ svg.setAttribute("viewBox", "0 0 " + W + " " + H);
139
+ svg.textContent = "";
140
+
141
+ if (!children || !children.length) {
142
+ svg.__tm.items = [];
143
+ const note = el("text", { x: W / 2, y: H / 2, "text-anchor": "middle",
144
+ fill: "var(--text-muted)", "font-size": 13 });
145
+ note.textContent = "Nothing to show here";
146
+ svg.appendChild(note);
147
+ return;
148
+ }
149
+
150
+ const placed = squarify(children, 0, 0, W, H);
151
+ const items = [];
152
+ const pending = []; // labels to measure once they are in the document
153
+ const frag = document.createDocumentFragment();
154
+
155
+ for (const cell of placed) {
156
+ const d = cell.item;
157
+ // Inset by half the gap on every side: neighbours each give up 1px, so
158
+ // the visible channel between two fills is exactly GAP.
159
+ const x = cell.x + GAP / 2, y = cell.y + GAP / 2;
160
+ const w = Math.max(0, cell.w - GAP), h = Math.max(0, cell.h - GAP);
161
+ if (w <= 0 || h <= 0) continue;
162
+
163
+ const isRest = d.kind === "rest";
164
+ const group = el("g", { class: "tile", tabindex: 0, role: "button", "data-i": items.length });
165
+ items.push(d);
166
+ group.setAttribute("aria-label",
167
+ d.name + ", " + opts.fmt(d.size) + (d.kind === "dir" ? ", folder" : ""));
168
+
169
+ group.appendChild(el("rect", {
170
+ x: x, y: y, width: w, height: h,
171
+ rx: Math.min(RADIUS, w / 2, h / 2),
172
+ fill: isRest ? "var(--neutral-mark)" : "var(--tile)",
173
+ "fill-opacity": d.kind === "file" ? 0.72 : 1,
174
+ }));
175
+
176
+ const ink = isRest ? "var(--tile-rest-ink)" : "var(--tile-ink)";
177
+ // Only label a tile when the text genuinely fits with padding — a clipped
178
+ // label is worse than none, and the table below carries every value.
179
+ if (w >= 58 && h >= 24) {
180
+ const label = el("text", {
181
+ x: x + 7, y: y + 16, fill: ink, "font-size": 12.5,
182
+ "font-weight": 600, "letter-spacing": "-.005em",
183
+ });
184
+ const glyph = GLYPH[d.kind] || "";
185
+ const prefix = glyph ? glyph + " " : "";
186
+ // The glyph rides in the same line box, so it has to come out of the
187
+ // budget too — otherwise every labelled tile runs two characters long
188
+ // and the name is sliced by the SVG clip instead of ellipsised. 6.9 is
189
+ // a deliberately pessimistic advance width: real names carry capitals
190
+ // and spaces, which run wider than the lowercase average.
191
+ const maxChars = Math.floor((w - 14) / 6.9) - prefix.length;
192
+ let name = d.name;
193
+ if (name.length > maxChars) name = name.slice(0, Math.max(1, maxChars - 1)) + "…";
194
+ label.textContent = prefix + name;
195
+ group.appendChild(label);
196
+ pending.push({ node: label, prefix: prefix, name: d.name, room: w - 14 });
197
+
198
+ if (h >= 42) {
199
+ const value = el("text", {
200
+ x: x + 7, y: y + 32, fill: ink, "fill-opacity": 0.85,
201
+ "font-size": 11.5, "font-weight": 500,
202
+ });
203
+ value.textContent = opts.fmt(d.size);
204
+ group.appendChild(value);
205
+ pending.push({ node: value, prefix: "", name: opts.fmt(d.size), room: w - 14 });
206
+ }
207
+ }
208
+ frag.appendChild(group);
209
+ }
210
+
211
+ svg.__tm.items = items;
212
+ svg.appendChild(frag);
213
+ for (const entry of pending) fitLabel(entry);
214
+ }
215
+
216
+ global.Treemap = { render: render, squarify: squarify };
217
+ })(window);