chamba 0.7.0 → 0.9.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.
- package/LICENSE +53 -14
- package/README.md +34 -8
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -210
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -261
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -36
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +4 -4
- package/templates/Dockerfile +15 -22
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +4 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
- package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
- package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-BkmyervY.js +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/home-3SeGzySM.css +1 -0
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-Cabb15F0.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/useNow-BHFKFraS.js +1 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +17 -0
- package/templates/pane-apps/client/specs/index.html +4 -2
- package/templates/pane-apps/server/home.mjs +12 -0
- package/templates/pane-apps/server/reviews.mjs +21 -0
- package/templates/pane-apps/server/specs.mjs +5 -1588
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +136 -0
- package/templates/skills/dx-review/references/acts.md +233 -0
- package/templates/skills/dx-spec/SKILL.md +230 -30
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +6 -52
- package/templates/skills/dx-spec/references/spec-guide.md +25 -5
- package/templates/skills/dx-spec/references/stages.md +149 -60
- package/templates/skills/dx-spec-execute/SKILL.md +217 -38
- package/templates/startup.mjs +22 -16
- package/templates/tool-helper.sh +168 -0
- package/templates/webterm/README.md +82 -27
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +55 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +95 -14
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +23 -0
- package/templates/webterm/public/app/dom.js +3 -0
- package/templates/webterm/public/app/frames.js +52 -18
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +24 -8
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +169 -0
- package/templates/webterm/public/app/pane-shell.js +240 -135
- package/templates/webterm/public/app/pane.js +39 -24
- package/templates/webterm/public/app/reviews-host.js +19 -0
- package/templates/webterm/public/app/specs-host.js +12 -218
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +318 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +22 -4
- package/templates/webterm/public/styles.css +195 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/home.js +58 -0
- package/templates/webterm/tools/index.js +26 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +137 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +70 -7
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
- package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
- package/templates/skills/dx-spec-config/SKILL.md +0 -313
- package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
- package/templates/specs.sh +0 -106
- package/templates/webterm/specs.js +0 -358
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// pane-shell.js - the pane itself: which tool is open in it, and how wide it is.
|
|
2
2
|
//
|
|
3
|
-
// The pane is furniture. What sits in it are tools -
|
|
4
|
-
// between them is the whole point of this module: a tool draws inside its
|
|
5
|
-
// the tab bar that says which tool is open, and the controls that say how
|
|
6
|
-
// here. A tool can therefore never move the pane, and never lie about which
|
|
3
|
+
// The pane is furniture. What sits in it are tools - Home (home-host.js), Specs (specs-host.js), Code Reviews
|
|
4
|
+
// (reviews-host.js) and Pages (pane.js) - and the line between them is the whole point of this module: a tool draws inside its
|
|
5
|
+
// own panel and nothing else, while the tab bar that says which tool is open, and the controls that say how
|
|
6
|
+
// wide the pane is, are drawn out here. A tool can therefore never move the pane, and never lie about which
|
|
7
|
+
// tool the user is looking at.
|
|
7
8
|
//
|
|
8
9
|
// Width has three states, and only one of them is a width. Between the two ends the divider drags anything,
|
|
9
10
|
// clamped as it always was. Past the near end the pane is put away and becomes the spine. Past the far end it
|
|
@@ -11,90 +12,158 @@
|
|
|
11
12
|
// drag's own clamps stay where they are. The grip is reachable in every one of them, and dragging it is
|
|
12
13
|
// always the way back.
|
|
13
14
|
//
|
|
14
|
-
// What the pane is belongs to the
|
|
15
|
-
//
|
|
15
|
+
// What the pane is belongs to the session it is beside - how wide, which tool, how big a page is drawn, and
|
|
16
|
+
// the reading theme - so switching sessions gives each one the pane it was left with, and a reload gives them
|
|
17
|
+
// all back. What is in a tool belongs to the session too, and the tool keeps that itself.
|
|
16
18
|
//
|
|
17
|
-
// This module imports
|
|
18
|
-
// load, so it has to be finished before any of them runs.
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// This module imports dom.js and pane-shape.js and nothing else, deliberately: the tools import it and
|
|
20
|
+
// register themselves as they load, so it has to be finished before any of them runs. Both of those are
|
|
21
|
+
// leaves - pane-shape.js is the shape, its rules and its store, and it holds no element and reaches no
|
|
22
|
+
// other module. Every rule about a shape lives there rather than here, so it can be run in a test; what is
|
|
23
|
+
// left here is the pane as a thing on the screen.
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
grip,
|
|
27
|
+
pane,
|
|
28
|
+
panebar,
|
|
29
|
+
paneCollapse,
|
|
30
|
+
paneFull,
|
|
31
|
+
paneHalf,
|
|
32
|
+
spine,
|
|
33
|
+
spineBadge,
|
|
34
|
+
toolHome,
|
|
35
|
+
toolPages,
|
|
36
|
+
toolReviews,
|
|
37
|
+
toolSpecs,
|
|
38
|
+
toolTabs,
|
|
39
|
+
} from "./dom.js";
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_FRACTION,
|
|
42
|
+
defaultShape,
|
|
43
|
+
draggedTo,
|
|
44
|
+
HOME,
|
|
45
|
+
isDefaultWidth,
|
|
46
|
+
mergeShape,
|
|
47
|
+
PAGES,
|
|
48
|
+
pruneShapes,
|
|
49
|
+
REVIEWS,
|
|
50
|
+
readShapes,
|
|
51
|
+
reopenedFrom,
|
|
52
|
+
SPECS,
|
|
53
|
+
} from "./pane-shape.js";
|
|
54
|
+
|
|
55
|
+
// The tools' ids come from the shape, which holds one of them. Re-exported here because this module is the
|
|
56
|
+
// pane, and a tool that asks whether it is open is asking the pane rather than the store.
|
|
57
|
+
export { HOME, PAGES, REVIEWS, SPECS };
|
|
21
58
|
|
|
22
59
|
// The tools, in the order their tabs are drawn. A tool is a tab, a panel, and a render function it hands over
|
|
23
60
|
// when it loads; the pane knows nothing else about any of them.
|
|
24
|
-
export const PAGES = "pages";
|
|
25
|
-
export const SPECS = "specs";
|
|
26
61
|
const TOOLS = [
|
|
27
|
-
{ id:
|
|
62
|
+
{ id: HOME, label: "Home", panel: toolHome },
|
|
28
63
|
{ id: SPECS, label: "Specs", panel: toolSpecs },
|
|
64
|
+
// "Reviews" on the tab and Code Reviews everywhere else: four tabs have to be readable at the width the
|
|
65
|
+
// pane reopens at, and the tab is the one place the tool's whole name does not fit.
|
|
66
|
+
{ id: REVIEWS, label: "Reviews", panel: toolReviews },
|
|
67
|
+
{ id: PAGES, label: "Pages", panel: toolPages },
|
|
29
68
|
];
|
|
30
69
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
// The window's own pane preferences: how wide, whether it is collapsed, whether it has the whole row, which
|
|
41
|
-
// tool is open, and how big a page is drawn in it. Not the session's - a pane is furniture, and furniture
|
|
42
|
-
// does not move because you looked at another tab.
|
|
43
|
-
const GEOMETRY_KEY = "webterm-pane";
|
|
44
|
-
|
|
45
|
-
let width = null;
|
|
46
|
-
// null means nobody has said. An untouched pane keeps out of the way while the tool it is open on has nothing
|
|
47
|
-
// in it, and is open once that tool has something - which is the first page of a session opening on arrival,
|
|
48
|
-
// as it always did. A click on the spine or on the collapse button is an answer, and from then on it is the
|
|
49
|
-
// only one that counts.
|
|
50
|
-
let collapsed = null;
|
|
51
|
-
// The far end: the pane has the whole row and the terminal is squeezed out.
|
|
52
|
-
let full = false;
|
|
53
|
-
let openTool = PAGES;
|
|
54
|
-
// How big a page is drawn. The pane applies it, so it is stored here with the rest of the furniture, and the
|
|
55
|
-
// Pages tool owns the control and the steps.
|
|
56
|
-
let pageSize = null;
|
|
57
|
-
// What the last render decided, for the two things that only need to know whether the pane is showing.
|
|
58
|
-
let shut = true;
|
|
70
|
+
// The one shape this window is drawing, and the session it belongs to. One and not a map of them: the store
|
|
71
|
+
// is shared by every window of this origin, so a shape held here from a session this window left is a copy
|
|
72
|
+
// that has had time to go stale. It is read again on every move to another session instead.
|
|
73
|
+
let sid = null;
|
|
74
|
+
let current = defaultShape();
|
|
75
|
+
// Whether a frame from the server named that session. The id this window opens with comes from its own
|
|
76
|
+
// storage and is the session it is about to ask for, which is enough to draw in and not enough to write
|
|
77
|
+
// under: the server may hand this window a different one, or none.
|
|
78
|
+
let told = false;
|
|
59
79
|
|
|
60
80
|
// id -> what that tool has to say for itself on its tab: how many things are waiting in it, and whether it
|
|
61
81
|
// has anything at all. A tool nobody has heard from yet is empty and waiting on nothing.
|
|
62
|
-
const EMPTY_MARK = { unread: 0, empty: true };
|
|
82
|
+
const EMPTY_MARK = { unread: 0, empty: true, counted: true };
|
|
63
83
|
const marks = new Map();
|
|
64
84
|
// id -> the tool's own render function, handed over as it loads.
|
|
65
85
|
const renderers = new Map();
|
|
66
86
|
|
|
67
|
-
// --- What
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const stored = JSON.parse(localStorage.getItem(GEOMETRY_KEY) ?? "{}");
|
|
71
|
-
if (typeof stored.width === "number" && stored.width >= COLLAPSE_AT) width = stored.width;
|
|
72
|
-
if (typeof stored.collapsed === "boolean") collapsed = stored.collapsed;
|
|
73
|
-
if (typeof stored.full === "boolean") full = stored.full;
|
|
74
|
-
if (TOOLS.some((tool) => tool.id === stored.tool)) openTool = stored.tool;
|
|
75
|
-
// The multiplier is stored rather than its position, so a window that was left at a size this version no
|
|
76
|
-
// longer offers falls back to the default instead of landing between two steps.
|
|
77
|
-
if (typeof stored.size === "number") pageSize = stored.size;
|
|
78
|
-
} catch {
|
|
79
|
-
// A disabled or full store only means the pane opens at its default width, on its default tool.
|
|
80
|
-
}
|
|
87
|
+
// --- What every session remembers --------------------------------------------------------------------------------------------------------
|
|
81
88
|
|
|
89
|
+
// Kept in the store, under the session this window is driving and no other. A window that has not been told
|
|
90
|
+
// its session yet writes nothing: the id it opened with may turn out to be another window's session, and the
|
|
91
|
+
// shape of that session is not this window's to set.
|
|
82
92
|
function remember() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if (!told) return;
|
|
94
|
+
mergeShape(localStorage, sid, current);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The shape of one session, off the store. A window with no session draws the defaults and stores nothing,
|
|
98
|
+
// because there is nobody to store it under.
|
|
99
|
+
function shapeOf(next) {
|
|
100
|
+
if (next === null) return defaultShape();
|
|
101
|
+
return readShapes(localStorage).get(next) ?? defaultShape();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Draw everything the shape decides: the pane's own chrome, and the tool open in it, which has a bar and a
|
|
105
|
+
// document to lay out at whatever width the pane now has.
|
|
106
|
+
function redraw() {
|
|
107
|
+
renderShell();
|
|
108
|
+
renderers.get(current.tool)?.();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The shape just changed, so keep it under this session and draw it. Every act on the pane ends here.
|
|
112
|
+
function reshape() {
|
|
113
|
+
remember();
|
|
114
|
+
redraw();
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* The window is beside that session now, so the pane is drawn in that session's own shape.
|
|
119
|
+
*
|
|
120
|
+
* The shape is read again here rather than kept from the load, so a window coming back to a session sees
|
|
121
|
+
* what it was left at, whichever window left it there. Called before the tools are told, because what they
|
|
122
|
+
* lay out depends on how wide the pane now is and on which of them is open in it.
|
|
123
|
+
*/
|
|
124
|
+
export function applySessionShape(next) {
|
|
125
|
+
const held = next ?? null;
|
|
126
|
+
// The first frame is worth acting on even where it names the session this window opened with: it is what
|
|
127
|
+
// makes the shape this window's to write, and the store may have moved since the page loaded.
|
|
128
|
+
if (held === sid && told) return;
|
|
129
|
+
// A drag in flight belongs to the session that is leaving. Ending it stores the width under that session
|
|
130
|
+
// rather than carrying the pointer into the next one's shape.
|
|
131
|
+
if (dragging) endDrag();
|
|
132
|
+
sid = held;
|
|
133
|
+
told = true;
|
|
134
|
+
current = shapeOf(held);
|
|
135
|
+
redraw();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A session that left the bar takes its pane shape with it, the way its unsent message and its page list do. */
|
|
139
|
+
export function prunePaneShapes(live) {
|
|
140
|
+
pruneShapes(localStorage, live);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** How big a page was last drawn in this session, or null when nobody has said. */
|
|
91
144
|
export function storedPageSize() {
|
|
92
|
-
return
|
|
145
|
+
return current.size;
|
|
93
146
|
}
|
|
94
147
|
|
|
95
|
-
/** The Pages tool's zoom, kept with the rest of the
|
|
148
|
+
/** The Pages tool's zoom, kept with the rest of the session's shape so one key has one writer. */
|
|
96
149
|
export function rememberPageSize(size) {
|
|
97
|
-
|
|
150
|
+
current.size = size;
|
|
151
|
+
remember();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The reading theme this session was left on, or null for the system preference. */
|
|
155
|
+
export function storedPaneTheme() {
|
|
156
|
+
return current.theme;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The Specs tool's reading theme, kept with the rest of the session's shape.
|
|
161
|
+
*
|
|
162
|
+
* The tool's frame has an opaque origin and therefore no storage of its own, so the pane holds the choice
|
|
163
|
+
* and hands it back with the session. Anything but the two palettes means the system preference.
|
|
164
|
+
*/
|
|
165
|
+
export function rememberPaneTheme(theme) {
|
|
166
|
+
current.theme = theme === "dark" || theme === "light" ? theme : null;
|
|
98
167
|
remember();
|
|
99
168
|
}
|
|
100
169
|
|
|
@@ -110,136 +179,155 @@ export function registerTool(id, render) {
|
|
|
110
179
|
|
|
111
180
|
/** Whether the pane is open on that tool right now. */
|
|
112
181
|
export function isToolOpen(id) {
|
|
113
|
-
return
|
|
182
|
+
return current.tool === id;
|
|
114
183
|
}
|
|
115
184
|
|
|
116
185
|
/**
|
|
117
186
|
* What a tool has to say on its tab: `unread` is what is waiting for the user in it, and `empty` is whether
|
|
118
187
|
* it has anything to show at all - which is what an untouched pane reads to decide whether to stay out of
|
|
119
188
|
* the way.
|
|
189
|
+
*
|
|
190
|
+
* `counted` is whether that number is worth showing. A tool that asks the user things counts them, because
|
|
191
|
+
* two questions waiting is a different day from one. A tool that only receives counts nothing the user
|
|
192
|
+
* would act on differently, so its tab says that something arrived and leaves the number out of it.
|
|
120
193
|
*/
|
|
121
|
-
export function noteTool(id, { unread = 0, empty = false } = {}) {
|
|
122
|
-
marks.set(id, { unread, empty });
|
|
194
|
+
export function noteTool(id, { unread = 0, empty = false, counted = true } = {}) {
|
|
195
|
+
marks.set(id, { unread, empty, counted });
|
|
123
196
|
renderShell();
|
|
124
197
|
}
|
|
125
198
|
|
|
126
|
-
/** Open a tool, drawing its panel. A tool the user chose is the tool this
|
|
199
|
+
/** Open a tool, drawing its panel. A tool the user chose is the tool this session comes back on. */
|
|
127
200
|
export function showTool(id) {
|
|
128
201
|
if (!TOOLS.some((tool) => tool.id === id)) return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
renderShell();
|
|
132
|
-
renderers.get(id)?.();
|
|
202
|
+
current.tool = id;
|
|
203
|
+
reshape();
|
|
133
204
|
}
|
|
134
205
|
|
|
135
206
|
function drawTabs() {
|
|
136
207
|
const tabs = TOOLS.map((tool) => {
|
|
137
208
|
const button = document.createElement("button");
|
|
138
209
|
button.type = "button";
|
|
139
|
-
button.className = tool.id ===
|
|
210
|
+
button.className = tool.id === current.tool ? "tool-tab on" : "tool-tab";
|
|
140
211
|
button.dataset.tool = tool.id;
|
|
141
212
|
button.append(tool.label);
|
|
142
213
|
// What is waiting in a tool nobody is looking at. The one thing a tab says beyond its name, so a tool
|
|
143
214
|
// never has to take the screen to be noticed.
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
215
|
+
const mark = marks.get(tool.id) ?? EMPTY_MARK;
|
|
216
|
+
if (mark.unread > 0) {
|
|
146
217
|
const badge = document.createElement("span");
|
|
147
|
-
badge.className = "tool-mark";
|
|
148
|
-
badge.textContent = String(
|
|
218
|
+
badge.className = mark.counted ? "tool-mark" : "tool-mark dot";
|
|
219
|
+
if (mark.counted) badge.textContent = String(mark.unread);
|
|
220
|
+
else badge.setAttribute("aria-label", "something new");
|
|
221
|
+
// The ring, for a tool something landed in a moment ago. This element is new on every redraw, so
|
|
222
|
+
// the ring would start from the beginning each time; a negative delay of what has already run
|
|
223
|
+
// puts it back where it was.
|
|
224
|
+
const ring = pulsing.get(tool.id);
|
|
225
|
+
if (ring) {
|
|
226
|
+
badge.classList.add("pulse");
|
|
227
|
+
badge.style.setProperty("--ring-delay", `-${Date.now() - ring.at}ms`);
|
|
228
|
+
}
|
|
149
229
|
button.append(badge);
|
|
150
230
|
}
|
|
151
231
|
button.addEventListener("click", () => showTool(tool.id));
|
|
152
232
|
return button;
|
|
153
233
|
});
|
|
154
234
|
toolTabs.replaceChildren(...tabs);
|
|
155
|
-
for (const tool of TOOLS) tool.panel.hidden = tool.id !==
|
|
235
|
+
for (const tool of TOOLS) tool.panel.hidden = tool.id !== current.tool;
|
|
156
236
|
}
|
|
157
237
|
|
|
158
238
|
// --- How wide ----------------------------------------------------------------------------------------------------------------------------
|
|
159
239
|
|
|
160
240
|
// How wide, put away, or the whole row. Its own function because a drag is the one thing that changes the
|
|
161
241
|
// pane without changing anything in it: sixty of these a second are cheap, and sixty full redraws are not.
|
|
162
|
-
function applyGeometry(
|
|
163
|
-
|
|
242
|
+
function applyGeometry() {
|
|
243
|
+
const { width, full, collapsed } = current;
|
|
244
|
+
pane.classList.toggle("collapsed", collapsed);
|
|
164
245
|
// Only one of the two can be true: a pane that is put away has no width to be full of.
|
|
165
|
-
pane.classList.toggle("full", full && !
|
|
246
|
+
pane.classList.toggle("full", full && !collapsed);
|
|
166
247
|
// The width goes into a custom property rather than straight onto `width`, so the stylesheet keeps the
|
|
167
248
|
// last word: the collapsed spine, the full row and the narrow-screen layout are rules, and an inline
|
|
168
249
|
// width would beat all three.
|
|
169
250
|
pane.style.setProperty("--pane-width", width === null ? `${DEFAULT_FRACTION * 100}%` : `${width}px`);
|
|
170
|
-
paneFull.classList.toggle("on", full && !
|
|
171
|
-
|
|
251
|
+
paneFull.classList.toggle("on", full && !collapsed);
|
|
252
|
+
paneHalf.classList.toggle("on", isDefaultWidth(current));
|
|
253
|
+
const back = full && !collapsed;
|
|
172
254
|
paneFull.title = back ? "Give the terminal its room back" : "Full width";
|
|
173
255
|
paneFull.setAttribute("aria-label", paneFull.title);
|
|
174
256
|
}
|
|
175
257
|
|
|
176
258
|
/** Draw the pane's own chrome: the tabs, the width, and what the spine says while the pane is away. */
|
|
177
259
|
export function renderShell() {
|
|
178
|
-
const mark = marks.get(
|
|
179
|
-
shut = collapsed === null ? mark.empty : collapsed;
|
|
260
|
+
const mark = marks.get(current.tool) ?? EMPTY_MARK;
|
|
180
261
|
pane.hidden = false;
|
|
181
262
|
// There is no width worth choosing for a tool with nothing in it.
|
|
182
|
-
grip.hidden = mark.empty &&
|
|
263
|
+
grip.hidden = mark.empty && current.collapsed;
|
|
183
264
|
|
|
184
|
-
applyGeometry(
|
|
265
|
+
applyGeometry();
|
|
185
266
|
drawTabs();
|
|
186
267
|
|
|
187
268
|
// The spine stands for the whole pane, so it carries what every tool is waiting on, not just the open one.
|
|
269
|
+
// It counts what the tools count, and falls back to the bare dot when all that arrived was uncounted:
|
|
270
|
+
// a number here has to mean the same thing it means on a tab.
|
|
188
271
|
let waiting = 0;
|
|
189
|
-
|
|
272
|
+
let arrived = false;
|
|
273
|
+
for (const entry of marks.values()) {
|
|
274
|
+
if (entry.unread <= 0) continue;
|
|
275
|
+
if (entry.counted) waiting += entry.unread;
|
|
276
|
+
else arrived = true;
|
|
277
|
+
}
|
|
190
278
|
spineBadge.textContent = waiting > 0 ? String(waiting) : "";
|
|
191
|
-
spineBadge.
|
|
279
|
+
spineBadge.classList.toggle("dot", waiting === 0 && arrived);
|
|
280
|
+
spineBadge.hidden = waiting === 0 && !arrived;
|
|
192
281
|
}
|
|
193
282
|
|
|
194
283
|
/**
|
|
195
284
|
* Draw the pane for the first time, on the tool this window was left on. Called from the entry rather than
|
|
196
285
|
* here, because the tools register as they load and there is no pane to draw until they all have.
|
|
197
286
|
*/
|
|
198
|
-
export function mountPane() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/** Whether the pane is drawn as the spine rather than as a pane. */
|
|
204
|
-
export function isPaneShut() {
|
|
205
|
-
return shut;
|
|
287
|
+
export function mountPane(startSid = null) {
|
|
288
|
+
sid = startSid ?? null;
|
|
289
|
+
current = shapeOf(sid);
|
|
290
|
+
redraw();
|
|
206
291
|
}
|
|
207
292
|
|
|
208
|
-
/** Whether the
|
|
293
|
+
/** Whether the pane is put away in this session, which is whether it is drawn as the spine. */
|
|
209
294
|
export function isPaneCollapsed() {
|
|
210
|
-
return collapsed
|
|
295
|
+
return current.collapsed;
|
|
211
296
|
}
|
|
212
297
|
|
|
213
298
|
// Away, and back. Two ways in and one way out, because the pane is put away far more often than it is
|
|
214
299
|
// resized: the button in the bar is the obvious one, and dragging the grip past the threshold is the other.
|
|
215
300
|
export function setPaneCollapsed(next) {
|
|
216
|
-
collapsed = next;
|
|
217
|
-
|
|
218
|
-
if (collapsed) {
|
|
301
|
+
current.collapsed = next;
|
|
302
|
+
if (current.collapsed) {
|
|
219
303
|
// Coming back to a pane that fills the window, with the terminal nowhere, is not what putting one
|
|
220
304
|
// away asks for. The width it had is remembered; the far end is not.
|
|
221
|
-
full = false;
|
|
305
|
+
current.full = false;
|
|
222
306
|
} else {
|
|
223
|
-
|
|
224
|
-
// from a window remembered narrower than this one, has to open into something a page fits in.
|
|
225
|
-
const floor = Math.min(window.innerWidth * MIN_OPEN_FRACTION, window.innerWidth * MAX_FRACTION);
|
|
226
|
-
if (width !== null && width < floor) width = floor;
|
|
307
|
+
current.width = reopenedFrom(current, window.innerWidth);
|
|
227
308
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
309
|
+
reshape();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Back to the width the pane opens at. Not a width of its own: the pane is left with none, and the
|
|
313
|
+
// stylesheet draws the default fraction, so a window resized afterwards leaves the pane at half rather
|
|
314
|
+
// than at the pixels half happened to be when the button was pressed.
|
|
315
|
+
function setHalf() {
|
|
316
|
+
current.width = null;
|
|
317
|
+
current.full = false;
|
|
318
|
+
current.collapsed = false;
|
|
319
|
+
reshape();
|
|
231
320
|
}
|
|
232
321
|
|
|
233
322
|
// The far end, and back. A press either way, and a drag of the grip is the other way back.
|
|
234
323
|
function setFull(next) {
|
|
235
|
-
full = next;
|
|
236
|
-
if (full) collapsed = false;
|
|
237
|
-
|
|
238
|
-
renderShell();
|
|
239
|
-
renderers.get(openTool)?.();
|
|
324
|
+
current.full = next;
|
|
325
|
+
if (current.full) current.collapsed = false;
|
|
326
|
+
reshape();
|
|
240
327
|
}
|
|
241
328
|
|
|
242
|
-
paneFull.addEventListener("click", () => setFull(!full));
|
|
329
|
+
paneFull.addEventListener("click", () => setFull(!current.full));
|
|
330
|
+
paneHalf.addEventListener("click", setHalf);
|
|
243
331
|
paneCollapse.addEventListener("click", () => setPaneCollapsed(true));
|
|
244
332
|
spine.addEventListener("click", () => setPaneCollapsed(false));
|
|
245
333
|
|
|
@@ -262,6 +350,33 @@ export function flashPane() {
|
|
|
262
350
|
arrivalTimer = setTimeout(() => pane.classList.remove("arriving"), ARRIVAL_MS);
|
|
263
351
|
}
|
|
264
352
|
|
|
353
|
+
// Three turns of the ring in styles.css, which runs for 1.6s each. The two numbers have to agree: the
|
|
354
|
+
// keyframes are there and what takes the ring off again is here.
|
|
355
|
+
const TAB_PULSE_MS = 4_800;
|
|
356
|
+
|
|
357
|
+
// id -> when that tool's ring started, and the timer that ends it. A tab is rebuilt on every redraw, so the
|
|
358
|
+
// ring cannot be left on the element the way the pane's own flash is - what holds it is this map.
|
|
359
|
+
const pulsing = new Map();
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Something landed in a tool the user is not looking at, so its tab draws a ring around its mark for three
|
|
363
|
+
* turns and then rests.
|
|
364
|
+
*
|
|
365
|
+
* A second arrival during those three turns starts them again, which is what the mark means: the tab says
|
|
366
|
+
* that something arrived just now, and the number beside it says how much is waiting.
|
|
367
|
+
*/
|
|
368
|
+
export function pulseTool(id) {
|
|
369
|
+
clearTimeout(pulsing.get(id)?.timer);
|
|
370
|
+
pulsing.set(id, {
|
|
371
|
+
at: Date.now(),
|
|
372
|
+
timer: setTimeout(() => {
|
|
373
|
+
pulsing.delete(id);
|
|
374
|
+
renderShell();
|
|
375
|
+
}, TAB_PULSE_MS),
|
|
376
|
+
});
|
|
377
|
+
renderShell();
|
|
378
|
+
}
|
|
379
|
+
|
|
265
380
|
// --- The divider -------------------------------------------------------------------------------------------------------------------------
|
|
266
381
|
|
|
267
382
|
// Dragging sets a pixel width until it stops making sense, and below the threshold the pane becomes the
|
|
@@ -275,37 +390,27 @@ let widthBeforeDrag = null;
|
|
|
275
390
|
|
|
276
391
|
grip.addEventListener("pointerdown", (event) => {
|
|
277
392
|
dragging = true;
|
|
278
|
-
widthBeforeDrag = width;
|
|
393
|
+
widthBeforeDrag = current.width;
|
|
279
394
|
document.body.classList.add("dragging");
|
|
280
395
|
grip.setPointerCapture(event.pointerId);
|
|
281
396
|
});
|
|
282
397
|
|
|
283
398
|
grip.addEventListener("pointermove", (event) => {
|
|
284
399
|
if (!dragging) return;
|
|
285
|
-
// A drag is a width, so it is the way out of the far end as well as the way into any other width.
|
|
286
|
-
full = false;
|
|
287
400
|
const wanted = Math.max(0, window.innerWidth - event.clientX - grip.offsetWidth / 2);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
collapsed = false;
|
|
293
|
-
width = Math.min(wanted, window.innerWidth * MAX_FRACTION);
|
|
294
|
-
}
|
|
295
|
-
// A drag is an answer too, so what it decides is what the next render starts from.
|
|
296
|
-
shut = collapsed;
|
|
297
|
-
applyGeometry(shut);
|
|
401
|
+
// A drag is a width, so it is the way into every state the pane has and the way out of each of them.
|
|
402
|
+
// Where each of those states begins is pane-shape.js's rule; what is left here is the pointer.
|
|
403
|
+
Object.assign(current, draggedTo(wanted, window.innerWidth, widthBeforeDrag));
|
|
404
|
+
applyGeometry();
|
|
298
405
|
});
|
|
299
406
|
|
|
300
407
|
function endDrag() {
|
|
301
408
|
if (!dragging) return;
|
|
302
409
|
dragging = false;
|
|
303
410
|
document.body.classList.remove("dragging");
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
renderShell();
|
|
308
|
-
renderers.get(openTool)?.();
|
|
411
|
+
// The shape changed under the pointer, and only the geometry followed it. A drag that crossed either
|
|
412
|
+
// threshold also changed which of the three states the pane is in, which the tool has to hear about.
|
|
413
|
+
reshape();
|
|
309
414
|
}
|
|
310
415
|
|
|
311
416
|
grip.addEventListener("pointerup", endDrag);
|