@sanohiro/casty 0.5.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 +21 -0
- package/README.ja.md +178 -0
- package/README.md +178 -0
- package/bin/casty +200 -0
- package/bin/casty.js +245 -0
- package/lib/bookmarks.js +32 -0
- package/lib/browser.js +305 -0
- package/lib/cdp.js +76 -0
- package/lib/chrome.js +155 -0
- package/lib/config.js +43 -0
- package/lib/hints.js +255 -0
- package/lib/input.js +545 -0
- package/lib/keys.js +51 -0
- package/lib/kitty.js +117 -0
- package/lib/urlbar.js +348 -0
- package/package.json +41 -0
package/lib/chrome.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Chrome process launch and binary detection
|
|
2
|
+
|
|
3
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
4
|
+
import { readdirSync, existsSync, rmSync, statSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const BROWSERS_DIR = join(homedir(), '.casty', 'browsers');
|
|
9
|
+
const IS_ARM_LINUX = process.platform === 'linux' && process.arch === 'arm64';
|
|
10
|
+
|
|
11
|
+
// Recursively search for headless shell binary in a Playwright-style directory
|
|
12
|
+
// Binary name varies by version: chrome-headless-shell or headless_shell
|
|
13
|
+
const HEADLESS_BINS = ['chrome-headless-shell', 'headless_shell'];
|
|
14
|
+
function findBinInDir(base) {
|
|
15
|
+
try {
|
|
16
|
+
for (const name of HEADLESS_BINS) {
|
|
17
|
+
// Check directly under base
|
|
18
|
+
const direct = join(base, name);
|
|
19
|
+
if (existsSync(direct) && statSync(direct).isFile()) return direct;
|
|
20
|
+
}
|
|
21
|
+
// Search subdirectories
|
|
22
|
+
for (const sub of readdirSync(base)) {
|
|
23
|
+
for (const name of HEADLESS_BINS) {
|
|
24
|
+
const bin = join(base, sub, name);
|
|
25
|
+
if (existsSync(bin) && statSync(bin).isFile()) return bin;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Detect Chrome binary
|
|
33
|
+
// Returns: { bin, headless } — headless=true means system Chrome (needs --headless=new)
|
|
34
|
+
export function findChrome() {
|
|
35
|
+
if (existsSync(BROWSERS_DIR)) {
|
|
36
|
+
const entries = readdirSync(BROWSERS_DIR).sort().reverse();
|
|
37
|
+
|
|
38
|
+
// 1. Chrome for Testing headless-shell (x86_64 only, skip ARM64 Linux)
|
|
39
|
+
if (!IS_ARM_LINUX) {
|
|
40
|
+
for (const dir of entries) {
|
|
41
|
+
if (!dir.startsWith('chrome-headless-shell-')) continue;
|
|
42
|
+
const bin = join(BROWSERS_DIR, dir, 'chrome-headless-shell');
|
|
43
|
+
if (existsSync(bin)) return { bin, headless: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Playwright format (all platforms)
|
|
48
|
+
for (const dir of entries) {
|
|
49
|
+
if (!dir.startsWith('chromium_headless_shell-')) continue;
|
|
50
|
+
const bin = findBinInDir(join(BROWSERS_DIR, dir));
|
|
51
|
+
if (bin) return { bin, headless: false };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. System Chrome/Chromium (fallback)
|
|
56
|
+
const candidates = [
|
|
57
|
+
'chromium-browser', 'chromium', 'google-chrome-stable', 'google-chrome',
|
|
58
|
+
];
|
|
59
|
+
for (const name of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
const path = execFileSync('which', [name], { encoding: 'utf8' }).trim();
|
|
62
|
+
if (path) return { bin: path, headless: true };
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Prevent profile bloat (delete caches/DBs except cookies/localStorage)
|
|
70
|
+
// Chrome processes these on startup, so accumulation slows launch dramatically
|
|
71
|
+
function cleanProfile(userDataDir) {
|
|
72
|
+
const def = join(userDataDir, 'Default');
|
|
73
|
+
if (!existsSync(def)) return;
|
|
74
|
+
const KEEP = new Set([
|
|
75
|
+
'Cookies', 'Cookies-journal',
|
|
76
|
+
'Local Storage',
|
|
77
|
+
'Preferences', 'Secure Preferences',
|
|
78
|
+
]);
|
|
79
|
+
try {
|
|
80
|
+
for (const entry of readdirSync(def)) {
|
|
81
|
+
if (KEEP.has(entry)) continue;
|
|
82
|
+
rmSync(join(def, entry), { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Launch Chrome, return DevTools WebSocket URL
|
|
88
|
+
export function launchChrome({ userDataDir, windowSize, args = [] } = {}) {
|
|
89
|
+
cleanProfile(userDataDir);
|
|
90
|
+
const chrome = findChrome();
|
|
91
|
+
if (!chrome) throw new Error('Chrome not found. Install chromium-browser or run ./bin/casty to install.');
|
|
92
|
+
|
|
93
|
+
const chromeArgs = [
|
|
94
|
+
'--remote-debugging-port=0',
|
|
95
|
+
'--no-first-run',
|
|
96
|
+
'--no-default-browser-check',
|
|
97
|
+
'--disable-blink-features=AutomationControlled',
|
|
98
|
+
'--auto-grant-permissions',
|
|
99
|
+
'--disable-component-update',
|
|
100
|
+
'--disable-default-apps',
|
|
101
|
+
'--disable-sync',
|
|
102
|
+
// Media playback (autoplay without user gesture in headless)
|
|
103
|
+
'--autoplay-policy=no-user-gesture-required',
|
|
104
|
+
'--disable-backgrounding-occluded-windows',
|
|
105
|
+
'--disable-features=PreloadMediaEngagementData,MediaEngagementBypassAutoplayPolicies',
|
|
106
|
+
// Reduce startup overhead
|
|
107
|
+
'--disable-breakpad',
|
|
108
|
+
'--disable-extensions',
|
|
109
|
+
'--disable-background-networking',
|
|
110
|
+
'--disable-background-timer-throttling',
|
|
111
|
+
'--disable-renderer-backgrounding',
|
|
112
|
+
'--metrics-recording-only',
|
|
113
|
+
'--password-store=basic',
|
|
114
|
+
'--use-mock-keychain',
|
|
115
|
+
`--user-data-dir=${userDataDir}`,
|
|
116
|
+
];
|
|
117
|
+
// System Chrome needs --headless=new (headless-shell doesn't)
|
|
118
|
+
if (chrome.headless) {
|
|
119
|
+
chromeArgs.push('--headless=new');
|
|
120
|
+
}
|
|
121
|
+
// Linux often lacks sandbox support (VMs, containers, etc.)
|
|
122
|
+
if (process.platform === 'linux') {
|
|
123
|
+
chromeArgs.push('--no-sandbox', '--disable-dev-shm-usage');
|
|
124
|
+
}
|
|
125
|
+
if (windowSize) {
|
|
126
|
+
chromeArgs.push(`--window-size=${windowSize.width},${windowSize.height}`);
|
|
127
|
+
}
|
|
128
|
+
chromeArgs.push(...args);
|
|
129
|
+
|
|
130
|
+
const proc = spawn(chrome.bin, chromeArgs, {
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Extract DevTools URL from stderr
|
|
135
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
136
|
+
let stderr = '';
|
|
137
|
+
const timeout = setTimeout(() => reject(new Error('Chrome startup timeout')), 15000);
|
|
138
|
+
|
|
139
|
+
proc.stderr.on('data', (chunk) => {
|
|
140
|
+
stderr += chunk.toString();
|
|
141
|
+
const m = stderr.match(/DevTools listening on (ws:\/\/[^\s]+)/);
|
|
142
|
+
if (m) {
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
const wsUrl = m[1];
|
|
145
|
+
const portMatch = wsUrl.match(/:(\d+)\//);
|
|
146
|
+
const port = portMatch ? parseInt(portMatch[1]) : 0;
|
|
147
|
+
resolve({ proc, wsUrl, port });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
proc.on('error', (err) => { clearTimeout(timeout); reject(err); });
|
|
152
|
+
proc.on('exit', (code) => { clearTimeout(timeout); reject(new Error(`Chrome exited with code ${code}\n${stderr}`)); });
|
|
153
|
+
|
|
154
|
+
return promise;
|
|
155
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Configuration
|
|
2
|
+
// Customizable via ~/.casty/config.json (falls back to defaults)
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
// Safely load and parse a JSON file (returns fallback on parse error or missing file)
|
|
9
|
+
export function loadJsonFile(filePath, fallback) {
|
|
10
|
+
try { return JSON.parse(readFileSync(filePath, 'utf8')); }
|
|
11
|
+
catch { return fallback; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const configPath = join(homedir(), '.casty', 'config.json');
|
|
15
|
+
|
|
16
|
+
// Detect system locale (e.g. "en-US", "ja", "zh-CN")
|
|
17
|
+
function detectLocale() {
|
|
18
|
+
// Intl API gives the most reliable result
|
|
19
|
+
try {
|
|
20
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
21
|
+
if (locale) return locale;
|
|
22
|
+
} catch {}
|
|
23
|
+
// Fallback to environment variables
|
|
24
|
+
const env = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || '';
|
|
25
|
+
const m = env.match(/^([a-z]{2}(?:[_-][A-Z]{2})?)/);
|
|
26
|
+
return m ? m[1].replace('_', '-') : 'en-US';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULTS = {
|
|
30
|
+
homeUrl: 'https://github.com/sanohiro/casty',
|
|
31
|
+
searchUrl: 'https://www.google.com/search?q=',
|
|
32
|
+
transport: 'auto', // 'auto' | 'file' | 'inline'
|
|
33
|
+
format: 'auto', // 'auto' | 'png' | 'jpeg'
|
|
34
|
+
language: detectLocale(), // System locale (override: "en-US", "ja", etc.)
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let _cache = null;
|
|
38
|
+
|
|
39
|
+
export function loadConfig() {
|
|
40
|
+
if (_cache) return _cache;
|
|
41
|
+
_cache = { ...DEFAULTS, ...loadJsonFile(configPath, {}) };
|
|
42
|
+
return _cache;
|
|
43
|
+
}
|
package/lib/hints.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Vimium-style hint mode
|
|
2
|
+
// Alt+F shows labels on clickable/focusable elements, select by typing the label
|
|
3
|
+
|
|
4
|
+
const HINT_CHARS = ['a', 's', 'd', 'f', 'j', 'k', 'l'];
|
|
5
|
+
const MAX_HINTS = HINT_CHARS.length * HINT_CHARS.length; // 49
|
|
6
|
+
|
|
7
|
+
// Collect elements + generate labels + inject overlay in a single evaluate call
|
|
8
|
+
function makeCollectAndOverlayScript(hintChars, maxHints) {
|
|
9
|
+
return `(() => {
|
|
10
|
+
try {
|
|
11
|
+
const old = document.getElementById('__casty_hints');
|
|
12
|
+
if (old) old.remove();
|
|
13
|
+
|
|
14
|
+
const CLICKABLE = 'a, button, [role="button"], [onclick], summary, [role="link"], [role="tab"], [tabindex]';
|
|
15
|
+
const FOCUSABLE = 'input, textarea, select, [contenteditable]';
|
|
16
|
+
const all = document.querySelectorAll(CLICKABLE + ', ' + FOCUSABLE);
|
|
17
|
+
|
|
18
|
+
const elems = [];
|
|
19
|
+
for (const el of all) {
|
|
20
|
+
if (elems.length >= ${maxHints}) break;
|
|
21
|
+
if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') continue;
|
|
22
|
+
const rect = el.getBoundingClientRect();
|
|
23
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
24
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
|
|
25
|
+
if (rect.right < 0 || rect.left > window.innerWidth) continue;
|
|
26
|
+
|
|
27
|
+
const isFocusable = el.matches('input, textarea, select, [contenteditable]');
|
|
28
|
+
elems.push({
|
|
29
|
+
x: Math.round(rect.left),
|
|
30
|
+
y: Math.round(rect.top),
|
|
31
|
+
cx: Math.round(rect.left + rect.width / 2),
|
|
32
|
+
cy: Math.round(rect.top + rect.height / 2),
|
|
33
|
+
type: isFocusable ? 'focus' : 'click',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (elems.length === 0) return JSON.stringify([]);
|
|
38
|
+
|
|
39
|
+
// Generate labels
|
|
40
|
+
const chars = ${JSON.stringify(hintChars)};
|
|
41
|
+
const labels = [];
|
|
42
|
+
if (elems.length <= chars.length) {
|
|
43
|
+
for (let i = 0; i < elems.length; i++) labels.push(chars[i]);
|
|
44
|
+
} else {
|
|
45
|
+
for (const c1 of chars) {
|
|
46
|
+
for (const c2 of chars) {
|
|
47
|
+
labels.push(c1 + c2);
|
|
48
|
+
if (labels.length >= elems.length) break;
|
|
49
|
+
}
|
|
50
|
+
if (labels.length >= elems.length) break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Inject overlay
|
|
55
|
+
const container = document.createElement('div');
|
|
56
|
+
container.id = '__casty_hints';
|
|
57
|
+
container.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;';
|
|
58
|
+
for (let i = 0; i < elems.length; i++) {
|
|
59
|
+
const h = elems[i];
|
|
60
|
+
h.label = labels[i];
|
|
61
|
+
const span = document.createElement('span');
|
|
62
|
+
span.textContent = labels[i].toUpperCase();
|
|
63
|
+
span.dataset.label = labels[i];
|
|
64
|
+
span.style.cssText = 'position:fixed;background:#FFEE00;color:#000;font:bold 12px monospace;border:1px solid #C38A00;border-radius:3px;padding:0 2px;z-index:2147483647;pointer-events:none;line-height:1.4;'
|
|
65
|
+
+ 'left:' + h.x + 'px;top:' + h.y + 'px;';
|
|
66
|
+
container.appendChild(span);
|
|
67
|
+
}
|
|
68
|
+
document.documentElement.appendChild(container);
|
|
69
|
+
|
|
70
|
+
return JSON.stringify(elems);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return JSON.stringify([]);
|
|
73
|
+
}
|
|
74
|
+
})()`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const COLLECT_OVERLAY_SCRIPT = makeCollectAndOverlayScript(HINT_CHARS, MAX_HINTS);
|
|
78
|
+
|
|
79
|
+
// Dim non-matching labels
|
|
80
|
+
function makeDimScript(buffer) {
|
|
81
|
+
return `(() => {
|
|
82
|
+
const c = document.getElementById('__casty_hints');
|
|
83
|
+
if (!c) return;
|
|
84
|
+
for (const span of c.children) {
|
|
85
|
+
const label = span.dataset.label;
|
|
86
|
+
span.style.opacity = label.startsWith('${buffer}') ? '1' : '0.2';
|
|
87
|
+
}
|
|
88
|
+
})()`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Remove overlay script
|
|
92
|
+
const REMOVE_OVERLAY_SCRIPT = `(() => {
|
|
93
|
+
const el = document.getElementById('__casty_hints');
|
|
94
|
+
if (el) el.remove();
|
|
95
|
+
})()`;
|
|
96
|
+
|
|
97
|
+
export class HintMode {
|
|
98
|
+
constructor(forceCapture) {
|
|
99
|
+
this.active = false;
|
|
100
|
+
this.buffer = '';
|
|
101
|
+
this._resolve = null;
|
|
102
|
+
this._client = null;
|
|
103
|
+
this._hints = [];
|
|
104
|
+
this._rawForceCapture = forceCapture || (() => {});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Ensure frame capture after DOM changes (immediate + delayed retry)
|
|
108
|
+
_forceCapture() {
|
|
109
|
+
this._rawForceCapture();
|
|
110
|
+
setTimeout(() => this._rawForceCapture(), 200);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Start hint mode — waits until user selects or cancels
|
|
114
|
+
async start(client) {
|
|
115
|
+
this._client = client;
|
|
116
|
+
this.buffer = '';
|
|
117
|
+
|
|
118
|
+
// Single evaluate: collect elements + generate labels + inject overlay
|
|
119
|
+
let hints;
|
|
120
|
+
try {
|
|
121
|
+
const { result } = await client.send('Runtime.evaluate', {
|
|
122
|
+
expression: COLLECT_OVERLAY_SCRIPT,
|
|
123
|
+
});
|
|
124
|
+
hints = JSON.parse(result.value);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error(`casty: hints failed: ${e.message}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!hints || hints.length === 0) {
|
|
131
|
+
console.error('casty: hints: no elements found');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.error(`casty: hints: ${hints.length} elements`);
|
|
136
|
+
this._hints = hints;
|
|
137
|
+
this.active = true;
|
|
138
|
+
this._forceCapture();
|
|
139
|
+
|
|
140
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
141
|
+
this._resolve = resolve;
|
|
142
|
+
return promise;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle input (only called while active)
|
|
146
|
+
async handleInput(str) {
|
|
147
|
+
if (!this.active) return false;
|
|
148
|
+
|
|
149
|
+
// Escape → cancel
|
|
150
|
+
if (str === '\x1b') {
|
|
151
|
+
await this._cancel();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Backspace → remove last buffer char
|
|
156
|
+
if (str === '\x7f' || str === '\x08') {
|
|
157
|
+
if (this.buffer.length > 0) {
|
|
158
|
+
this.buffer = this.buffer.slice(0, -1);
|
|
159
|
+
await this._updateDim();
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Ignore non-hint characters
|
|
165
|
+
const ch = str.toLowerCase();
|
|
166
|
+
if (ch.length !== 1 || !HINT_CHARS.includes(ch)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.buffer += ch;
|
|
171
|
+
|
|
172
|
+
// Exact match check
|
|
173
|
+
const exact = this._hints.find(h => h.label === this.buffer);
|
|
174
|
+
if (exact) {
|
|
175
|
+
await this._selectHint(exact);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Prefix match check
|
|
180
|
+
const partial = this._hints.some(h => h.label.startsWith(this.buffer));
|
|
181
|
+
if (!partial) {
|
|
182
|
+
this.buffer = '';
|
|
183
|
+
await this._updateDim();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Partial match — dim non-matching labels
|
|
188
|
+
await this._updateDim();
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async _updateDim() {
|
|
193
|
+
try {
|
|
194
|
+
await this._client.send('Runtime.evaluate', {
|
|
195
|
+
expression: this.buffer ? makeDimScript(this.buffer) : makeDimScript(''),
|
|
196
|
+
});
|
|
197
|
+
this._forceCapture();
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async _selectHint(hint) {
|
|
202
|
+
await this._removeOverlay();
|
|
203
|
+
|
|
204
|
+
if (hint.type === 'focus') {
|
|
205
|
+
try {
|
|
206
|
+
await this._client.send('Runtime.evaluate', {
|
|
207
|
+
expression: `(() => {
|
|
208
|
+
const all = document.querySelectorAll('input, textarea, select, [contenteditable]');
|
|
209
|
+
for (const el of all) {
|
|
210
|
+
const r = el.getBoundingClientRect();
|
|
211
|
+
if (Math.round(r.left + r.width/2) === ${hint.cx} && Math.round(r.top + r.height/2) === ${hint.cy}) {
|
|
212
|
+
el.focus();
|
|
213
|
+
el.click();
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
})()`,
|
|
218
|
+
});
|
|
219
|
+
} catch {}
|
|
220
|
+
} else {
|
|
221
|
+
try {
|
|
222
|
+
await this._client.send('Input.dispatchMouseEvent', {
|
|
223
|
+
type: 'mousePressed', x: hint.cx, y: hint.cy, button: 'left', clickCount: 1,
|
|
224
|
+
});
|
|
225
|
+
await this._client.send('Input.dispatchMouseEvent', {
|
|
226
|
+
type: 'mouseReleased', x: hint.cx, y: hint.cy, button: 'left',
|
|
227
|
+
});
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this._done();
|
|
232
|
+
this._forceCapture();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async _cancel() {
|
|
236
|
+
await this._removeOverlay();
|
|
237
|
+
this._done();
|
|
238
|
+
this._forceCapture();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_done() {
|
|
242
|
+
this.active = false;
|
|
243
|
+
this.buffer = '';
|
|
244
|
+
this._hints = [];
|
|
245
|
+
if (this._resolve) { this._resolve(); this._resolve = null; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async _removeOverlay() {
|
|
249
|
+
try {
|
|
250
|
+
await this._client.send('Runtime.evaluate', {
|
|
251
|
+
expression: REMOVE_OVERLAY_SCRIPT,
|
|
252
|
+
});
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
}
|