@franzzemen/brokenstock-shell 0.9.1 → 0.9.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, isDevMode, effect, untracked, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, DestroyRef, NgZone, viewChild, afterNextRender, output } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, isDevMode, effect, untracked, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, DestroyRef, NgZone, PLATFORM_ID, viewChild, afterNextRender, output } from '@angular/core';
|
|
3
3
|
import * as i2$1 from '@angular/router';
|
|
4
4
|
import { Router, RouterLink, RouterLinkActive, ActivatedRoute, RouterOutlet } from '@angular/router';
|
|
5
5
|
import { BEARER_TOKEN_REGEX, HttpHeader, USERNAME_REGEX, PASSWORD_REGEX } from '@franzzemen/utility';
|
|
@@ -11,7 +11,7 @@ import { NG_VALIDATORS, FormsModule } from '@angular/forms';
|
|
|
11
11
|
import { Marked } from 'marked';
|
|
12
12
|
import { gfmHeadingId } from 'marked-gfm-heading-id';
|
|
13
13
|
import fm from 'front-matter';
|
|
14
|
-
import { NgClass, CommonModule, DOCUMENT, DatePipe } from '@angular/common';
|
|
14
|
+
import { NgClass, CommonModule, DOCUMENT, isPlatformBrowser, DatePipe } from '@angular/common';
|
|
15
15
|
import { Title, Meta } from '@angular/platform-browser';
|
|
16
16
|
import { z } from 'zod/v4';
|
|
17
17
|
import * as i1 from '@angular/material/icon';
|
|
@@ -2844,6 +2844,98 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2844
2844
|
args: [{ providedIn: 'root' }]
|
|
2845
2845
|
}], ctorParameters: () => [] });
|
|
2846
2846
|
|
|
2847
|
+
/*
|
|
2848
|
+
Created by Franz Zemen 2026-10-18
|
|
2849
|
+
License Type: UNLICENSED
|
|
2850
|
+
|
|
2851
|
+
Keys a screen owns while it is open: trading hotkeys (live-trading-foundation.prd.md D24, E8;
|
|
2852
|
+
live-trading.prd.md D34).
|
|
2853
|
+
|
|
2854
|
+
`KeyboardShortcutService` cannot do this job, and is not changed to: it allows only Alt and Shift
|
|
2855
|
+
and switches itself off inside any input. Hotkeys need the opposite on both counts:
|
|
2856
|
+
|
|
2857
|
+
- **Ctrl is allowed** (the defaults are Shift+F1 and Ctrl+F1). Meta (the Windows/Command key) never
|
|
2858
|
+
matches.
|
|
2859
|
+
- **They work while the cursor is in the command terminal**, the one input that is not a place to
|
|
2860
|
+
type a number. They do NOT fire while focus is in any other input, textarea, select or editable
|
|
2861
|
+
element: a key there is typing.
|
|
2862
|
+
- **Matched on `event.code`**, the physical key, never the character produced.
|
|
2863
|
+
- **Held-down repeats are ignored** (`event.repeat`): one order per press.
|
|
2864
|
+
- **Only on the routes a registration names.**
|
|
2865
|
+
|
|
2866
|
+
When a handler takes a key, the browser's own action for it is prevented and it goes no further.
|
|
2867
|
+
*/
|
|
2868
|
+
/** The class the command terminal's input carries; keys typed there still reach a screen's keys. */
|
|
2869
|
+
const TERMINAL_INPUT_CLASS = 'terminal-input';
|
|
2870
|
+
/** True when the key is typed into a box other than the command terminal. */
|
|
2871
|
+
function isTypingElsewhere(target) {
|
|
2872
|
+
const el = target;
|
|
2873
|
+
if (!el || typeof el.tagName !== 'string')
|
|
2874
|
+
return false;
|
|
2875
|
+
if (el.classList?.contains(TERMINAL_INPUT_CLASS))
|
|
2876
|
+
return false;
|
|
2877
|
+
if (el.isContentEditable)
|
|
2878
|
+
return true;
|
|
2879
|
+
const tag = el.tagName.toUpperCase();
|
|
2880
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT')
|
|
2881
|
+
return true;
|
|
2882
|
+
if (tag !== 'INPUT')
|
|
2883
|
+
return false;
|
|
2884
|
+
const type = (el.type || 'text').toLowerCase();
|
|
2885
|
+
return !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file', 'image'].includes(type);
|
|
2886
|
+
}
|
|
2887
|
+
/** Exactly this combination: the key, and these modifiers and no others. */
|
|
2888
|
+
function comboMatches(combo, e) {
|
|
2889
|
+
return e.code === combo.code && e.ctrlKey === combo.ctrl && e.shiftKey === combo.shift && e.altKey === combo.alt && !e.metaKey;
|
|
2890
|
+
}
|
|
2891
|
+
/** Whether a keydown may reach a screen's keys at all. */
|
|
2892
|
+
function isEligibleKey(e) {
|
|
2893
|
+
return !e.repeat && !e.isComposing && !isTypingElsewhere(e.target);
|
|
2894
|
+
}
|
|
2895
|
+
class ScreenKeysService {
|
|
2896
|
+
#document = inject(DOCUMENT);
|
|
2897
|
+
#zone = inject(NgZone);
|
|
2898
|
+
#router = inject(Router);
|
|
2899
|
+
#registrations = new Set();
|
|
2900
|
+
#listening = false;
|
|
2901
|
+
constructor() {
|
|
2902
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID)))
|
|
2903
|
+
return;
|
|
2904
|
+
inject(DestroyRef).onDestroy(() => this.#document.removeEventListener('keydown', this.#onKey, true));
|
|
2905
|
+
}
|
|
2906
|
+
/** Registers keys for a screen; returns the function that removes them. */
|
|
2907
|
+
register(registration) {
|
|
2908
|
+
this.#registrations.add(registration);
|
|
2909
|
+
if (!this.#listening) {
|
|
2910
|
+
this.#listening = true;
|
|
2911
|
+
// Capture phase, so a screen's key is seen before a focused control can act on it.
|
|
2912
|
+
this.#zone.runOutsideAngular(() => this.#document.addEventListener('keydown', this.#onKey, true));
|
|
2913
|
+
}
|
|
2914
|
+
return () => this.#registrations.delete(registration);
|
|
2915
|
+
}
|
|
2916
|
+
#onKey = (e) => {
|
|
2917
|
+
if (this.#registrations.size === 0 || !isEligibleKey(e))
|
|
2918
|
+
return;
|
|
2919
|
+
const url = this.#router.url;
|
|
2920
|
+
for (const r of this.#registrations) {
|
|
2921
|
+
if (!matchesRoute(url, r.routes))
|
|
2922
|
+
continue;
|
|
2923
|
+
const taken = this.#zone.run(() => r.handle(e));
|
|
2924
|
+
if (taken) {
|
|
2925
|
+
e.preventDefault();
|
|
2926
|
+
e.stopPropagation();
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ScreenKeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2932
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ScreenKeysService, providedIn: 'root' }); }
|
|
2933
|
+
}
|
|
2934
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ScreenKeysService, decorators: [{
|
|
2935
|
+
type: Injectable,
|
|
2936
|
+
args: [{ providedIn: 'root' }]
|
|
2937
|
+
}], ctorParameters: () => [] });
|
|
2938
|
+
|
|
2847
2939
|
const FAQ_USAGE = 'faq\n' +
|
|
2848
2940
|
'faq <key>\n' +
|
|
2849
2941
|
'faq search <term>\n' +
|
|
@@ -3068,6 +3160,12 @@ class PendingPromptState {
|
|
|
3068
3160
|
const CommandInputSchema = z.string().trim().min(1, 'Command cannot be empty');
|
|
3069
3161
|
class CommandService {
|
|
3070
3162
|
#prompts;
|
|
3163
|
+
/**
|
|
3164
|
+
* True while a command (or an answer) runs. A question it asks is written to the history only
|
|
3165
|
+
* after the command's own lines, so it reads as the last thing said, under the preview it is about.
|
|
3166
|
+
*/
|
|
3167
|
+
#running;
|
|
3168
|
+
#deferredQuestion;
|
|
3071
3169
|
constructor() {
|
|
3072
3170
|
this.history = signal([], /* @ts-ignore */
|
|
3073
3171
|
...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
|
|
@@ -3084,6 +3182,11 @@ class CommandService {
|
|
|
3084
3182
|
this.pending = signal(null, /* @ts-ignore */
|
|
3085
3183
|
...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
|
|
3086
3184
|
this.#prompts = new PendingPromptState();
|
|
3185
|
+
/**
|
|
3186
|
+
* True while a command (or an answer) runs. A question it asks is written to the history only
|
|
3187
|
+
* after the command's own lines, so it reads as the last thing said, under the preview it is about.
|
|
3188
|
+
*/
|
|
3189
|
+
this.#running = false;
|
|
3087
3190
|
this.authService = inject(AuthenticationService);
|
|
3088
3191
|
this.registry = inject(CommandRegistryService);
|
|
3089
3192
|
this.router = inject(Router);
|
|
@@ -3108,11 +3211,25 @@ class CommandService {
|
|
|
3108
3211
|
ask(prompt) {
|
|
3109
3212
|
this.#prompts.ask(prompt);
|
|
3110
3213
|
this.pending.set(prompt);
|
|
3214
|
+
if (this.#running)
|
|
3215
|
+
this.#deferredQuestion = prompt;
|
|
3216
|
+
else
|
|
3217
|
+
this.#writeQuestion(prompt);
|
|
3218
|
+
}
|
|
3219
|
+
#writeQuestion(prompt) {
|
|
3111
3220
|
this.history.update(h => [...h, {
|
|
3112
3221
|
input: '',
|
|
3113
3222
|
result: { success: true, message: prompt.question, timestamp: new Date(), severity: 'info' },
|
|
3114
3223
|
}]);
|
|
3115
3224
|
}
|
|
3225
|
+
/** After a command's or an answer's own lines: the question it asked, if it is still open. */
|
|
3226
|
+
#finishRun() {
|
|
3227
|
+
this.#running = false;
|
|
3228
|
+
const q = this.#deferredQuestion;
|
|
3229
|
+
this.#deferredQuestion = undefined;
|
|
3230
|
+
if (q && this.#prompts.current === q)
|
|
3231
|
+
this.#writeQuestion(q);
|
|
3232
|
+
}
|
|
3116
3233
|
/** Esc: closes the open question, if any. True when there was one. */
|
|
3117
3234
|
cancelPending() {
|
|
3118
3235
|
if (!this.#prompts.cancel())
|
|
@@ -3135,11 +3252,9 @@ class CommandService {
|
|
|
3135
3252
|
this.currentInput.set('');
|
|
3136
3253
|
const record = (result) => {
|
|
3137
3254
|
this.history.update(h => [...h, { input: echo, result }]);
|
|
3138
|
-
|
|
3139
|
-
const next = this.#prompts.current;
|
|
3140
|
-
if (next && next !== this.pending())
|
|
3141
|
-
this.pending.set(next);
|
|
3255
|
+
this.#finishRun();
|
|
3142
3256
|
};
|
|
3257
|
+
this.#running = true;
|
|
3143
3258
|
let out;
|
|
3144
3259
|
try {
|
|
3145
3260
|
out = prompt.answer(text);
|
|
@@ -3200,7 +3315,14 @@ class CommandService {
|
|
|
3200
3315
|
// masked under `login` stays masked under any other word for it).
|
|
3201
3316
|
const def = this.registry.resolve(command)?.command;
|
|
3202
3317
|
const displayInput = def?.sanitizeInput ? def.sanitizeInput(input) : input;
|
|
3203
|
-
|
|
3318
|
+
this.#running = true;
|
|
3319
|
+
let resultOrPromise;
|
|
3320
|
+
try {
|
|
3321
|
+
resultOrPromise = this.#dispatch(command, parts.slice(1));
|
|
3322
|
+
}
|
|
3323
|
+
catch (err) {
|
|
3324
|
+
resultOrPromise = { success: false, message: err?.message ?? String(err), timestamp: new Date() };
|
|
3325
|
+
}
|
|
3204
3326
|
this.currentInput.set('');
|
|
3205
3327
|
if (resultOrPromise instanceof Promise) {
|
|
3206
3328
|
this.isProcessing.set(true);
|
|
@@ -3214,7 +3336,10 @@ class CommandService {
|
|
|
3214
3336
|
this.history.update(h => [...h, { input: displayInput, result: historyResult, repeatCount: 1 }]);
|
|
3215
3337
|
}
|
|
3216
3338
|
})
|
|
3217
|
-
.finally(() =>
|
|
3339
|
+
.finally(() => {
|
|
3340
|
+
this.#finishRun();
|
|
3341
|
+
this.isProcessing.set(false);
|
|
3342
|
+
});
|
|
3218
3343
|
}
|
|
3219
3344
|
else {
|
|
3220
3345
|
if (resultOrPromise.isHelp && resultOrPromise.message) {
|
|
@@ -3224,6 +3349,7 @@ class CommandService {
|
|
|
3224
3349
|
if (!this.#coalesce(displayInput)) {
|
|
3225
3350
|
this.history.update(h => [...h, { input: displayInput, result: historyResult, repeatCount: 1 }]);
|
|
3226
3351
|
}
|
|
3352
|
+
this.#finishRun();
|
|
3227
3353
|
}
|
|
3228
3354
|
}
|
|
3229
3355
|
#coalesce(input) {
|
|
@@ -4456,5 +4582,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
4456
4582
|
* Generated bundle index. Do not edit.
|
|
4457
4583
|
*/
|
|
4458
4584
|
|
|
4459
|
-
export { AuthenticationService, AuthenticatorError, AuthenticatorSettings, CLEAR_USAGE, CODE_STEP, CodeDialog, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PendingPromptState, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RecoveryCodes, RegexValidatorDirective, SESSION_TOKEN_KEY, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_PHONE_MEDIA_QUERY, SHELL_POST_LOGIN_ROUTE, SHELL_SHARED_SESSION, SessionTokenStore, Shell, SideMenu, StatusBar, TOO_MANY_ATTEMPTS, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, answerEcho, audienceAllowed, authGuard, authInterceptor, authenticatorMessage, buildFallbackChain, buildLoginBody, decideRenewal, guestGuard, isYes, matchesRoute, openCodeDialog, provideShellConfig, recoveryCodesFile, tokenize };
|
|
4585
|
+
export { AuthenticationService, AuthenticatorError, AuthenticatorSettings, CLEAR_USAGE, CODE_STEP, CodeDialog, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PendingPromptState, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RecoveryCodes, RegexValidatorDirective, SESSION_TOKEN_KEY, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_PHONE_MEDIA_QUERY, SHELL_POST_LOGIN_ROUTE, SHELL_SHARED_SESSION, ScreenKeysService, SessionTokenStore, Shell, SideMenu, StatusBar, TERMINAL_INPUT_CLASS, TOO_MANY_ATTEMPTS, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, answerEcho, audienceAllowed, authGuard, authInterceptor, authenticatorMessage, buildFallbackChain, buildLoginBody, comboMatches, decideRenewal, guestGuard, isEligibleKey, isTypingElsewhere, isYes, matchesRoute, openCodeDialog, provideShellConfig, recoveryCodesFile, tokenize };
|
|
4460
4586
|
//# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map
|