@maccesar/aiskills 1.22.0 → 1.23.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/README.md +84 -75
- package/lib/config.js +1 -0
- package/package.json +1 -1
- package/skills/technical-demo-videos/SKILL.md +146 -0
- package/skills/technical-demo-videos/agents/openai.yaml +4 -0
- package/skills/technical-demo-videos/evals/evals.json +62 -0
- package/skills/technical-demo-videos/references/audio-timing.md +40 -0
- package/skills/technical-demo-videos/references/package-contract.md +33 -0
- package/skills/technical-demo-videos/references/publishing-metadata.md +36 -0
- package/skills/technical-demo-videos/references/recording-workflow.md +40 -0
- package/skills/technical-demo-videos/references/story-direction.md +37 -0
- package/skills/technical-demo-videos/references/vertical-social-video.md +24 -0
- package/skills/technical-demo-videos/references/vscode-default-profile.md +49 -0
- package/skills/technical-demo-videos/references/youtube-master.md +42 -0
- package/skills/technical-demo-videos/references/youtube-publishing.md +126 -0
- package/skills/technical-demo-videos/scripts/events_to_cues.py +89 -0
- package/skills/technical-demo-videos/scripts/normalize_youtube_master.py +210 -0
- package/skills/technical-demo-videos/scripts/recording_runtime.py +427 -0
- package/skills/technical-demo-videos/scripts/youtube_publish.py +517 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reusable primitives copied into an approved technical-video package."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import signal
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from statistics import median
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_VSCODE_PROFILE = {
|
|
19
|
+
'logical_display': (1920, 1080),
|
|
20
|
+
'minimum_window_size': (1850, 1000),
|
|
21
|
+
'auxiliary_panel_key_code': 19,
|
|
22
|
+
'terminal_open_delay': 1.5,
|
|
23
|
+
'terminal_drag_x': 645,
|
|
24
|
+
'terminal_target_y': 720,
|
|
25
|
+
'terminal_search_y': (90, 950)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
SWIFT_DRAG_SOURCE = r'''
|
|
29
|
+
import CoreGraphics
|
|
30
|
+
import Foundation
|
|
31
|
+
|
|
32
|
+
guard CommandLine.arguments.count == 5,
|
|
33
|
+
let startX = Double(CommandLine.arguments[1]),
|
|
34
|
+
let startY = Double(CommandLine.arguments[2]),
|
|
35
|
+
let endX = Double(CommandLine.arguments[3]),
|
|
36
|
+
let endY = Double(CommandLine.arguments[4]) else {
|
|
37
|
+
exit(2)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let start = CGPoint(x: startX, y: startY)
|
|
41
|
+
let end = CGPoint(x: endX, y: endY)
|
|
42
|
+
CGEvent(mouseEventSource: nil, mouseType: .mouseMoved,
|
|
43
|
+
mouseCursorPosition: start, mouseButton: .left)?.post(tap: .cghidEventTap)
|
|
44
|
+
usleep(100_000)
|
|
45
|
+
CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown,
|
|
46
|
+
mouseCursorPosition: start, mouseButton: .left)?.post(tap: .cghidEventTap)
|
|
47
|
+
for step in 1...20 {
|
|
48
|
+
let progress = Double(step) / 20.0
|
|
49
|
+
let point = CGPoint(
|
|
50
|
+
x: startX + ((endX - startX) * progress),
|
|
51
|
+
y: startY + ((endY - startY) * progress)
|
|
52
|
+
)
|
|
53
|
+
CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged,
|
|
54
|
+
mouseCursorPosition: point, mouseButton: .left)?.post(tap: .cghidEventTap)
|
|
55
|
+
usleep(15_000)
|
|
56
|
+
}
|
|
57
|
+
CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp,
|
|
58
|
+
mouseCursorPosition: end, mouseButton: .left)?.post(tap: .cghidEventTap)
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MacVsCodeRecording:
|
|
63
|
+
def __init__(
|
|
64
|
+
self, source_project, output_video, output_events, slug,
|
|
65
|
+
copies_root=None, display_number=1
|
|
66
|
+
):
|
|
67
|
+
self.source_project = Path(source_project).resolve()
|
|
68
|
+
self.output_video = Path(output_video).resolve()
|
|
69
|
+
self.output_events = Path(output_events).resolve()
|
|
70
|
+
self.slug = slug
|
|
71
|
+
configured_root = copies_root or os.environ.get('TECHNICAL_DEMO_COPIES_ROOT')
|
|
72
|
+
self.copies_root = Path(configured_root).expanduser().resolve() if configured_root else Path.home() / 'TechnicalDemos'
|
|
73
|
+
self.display_number = int(display_number)
|
|
74
|
+
self.temp_root = None
|
|
75
|
+
self.project = None
|
|
76
|
+
self.window_slug = None
|
|
77
|
+
self.capture = None
|
|
78
|
+
self.drag_helper = None
|
|
79
|
+
self.events = []
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _apple_string(value):
|
|
83
|
+
return str(value).replace('\\', '\\\\').replace('"', '\\"')
|
|
84
|
+
|
|
85
|
+
def preflight(self, required_files=(), forbidden_files=()):
|
|
86
|
+
for tool in (
|
|
87
|
+
'code', 'magick', 'osascript', 'screencapture', 'swift', 'swiftc'
|
|
88
|
+
):
|
|
89
|
+
if shutil.which(tool) is None:
|
|
90
|
+
raise RuntimeError(f'missing required tool: {tool}')
|
|
91
|
+
if not self.source_project.is_dir():
|
|
92
|
+
raise RuntimeError(f'project not found: {self.source_project}')
|
|
93
|
+
missing = [path for path in required_files if not (self.source_project / path).exists()]
|
|
94
|
+
if missing:
|
|
95
|
+
raise RuntimeError('missing project files: ' + ', '.join(missing))
|
|
96
|
+
present = [path for path in forbidden_files if (self.source_project / path).exists()]
|
|
97
|
+
if present:
|
|
98
|
+
raise RuntimeError('source already contains generated files: ' + ', '.join(present))
|
|
99
|
+
for artifact in (self.output_video, self.output_events):
|
|
100
|
+
if artifact.exists():
|
|
101
|
+
raise RuntimeError(f'refusing to overwrite: {artifact}')
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def logical_display_size():
|
|
105
|
+
source = (
|
|
106
|
+
'import AppKit; '
|
|
107
|
+
'let f = NSScreen.main!.frame; '
|
|
108
|
+
'print(Int(f.width), Int(f.height))'
|
|
109
|
+
)
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
['swift', '-e', source], check=True, capture_output=True, text=True
|
|
112
|
+
)
|
|
113
|
+
width, height = result.stdout.strip().split()
|
|
114
|
+
return int(width), int(height)
|
|
115
|
+
|
|
116
|
+
def create_copy(self):
|
|
117
|
+
copies_root = self.copies_root
|
|
118
|
+
copies_root.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
if not copies_root.is_dir():
|
|
120
|
+
raise RuntimeError(f'recording copies folder not found: {copies_root}')
|
|
121
|
+
self.temp_root = Path(tempfile.mkdtemp(prefix=f'{self.slug}-recording.'))
|
|
122
|
+
self.project = Path(tempfile.mkdtemp(prefix=f'{self.slug}.', dir=copies_root))
|
|
123
|
+
self.window_slug = self.project.name
|
|
124
|
+
shutil.copytree(self.source_project, self.project, dirs_exist_ok=True)
|
|
125
|
+
self.drag_helper = self.temp_root / 'drag'
|
|
126
|
+
subprocess.run(
|
|
127
|
+
['swiftc', '-o', str(self.drag_helper), '-'],
|
|
128
|
+
input=SWIFT_DRAG_SOURCE, check=True, capture_output=True, text=True
|
|
129
|
+
)
|
|
130
|
+
return self.project
|
|
131
|
+
|
|
132
|
+
def open_vscode(self):
|
|
133
|
+
subprocess.run(['code', '-n', str(self.project)], check=True)
|
|
134
|
+
|
|
135
|
+
def targeted(self, body, capture_output=False):
|
|
136
|
+
slug = self._apple_string(self.window_slug)
|
|
137
|
+
script = f'''
|
|
138
|
+
set slug to "{slug}"
|
|
139
|
+
tell application "System Events"
|
|
140
|
+
tell process "Code"
|
|
141
|
+
set targetWindow to missing value
|
|
142
|
+
repeat with candidateWindow in windows
|
|
143
|
+
if name of candidateWindow contains slug then
|
|
144
|
+
set targetWindow to candidateWindow
|
|
145
|
+
exit repeat
|
|
146
|
+
end if
|
|
147
|
+
end repeat
|
|
148
|
+
if targetWindow is missing value then error "Target window not found: " & slug
|
|
149
|
+
perform action "AXRaise" of targetWindow
|
|
150
|
+
set frontmost to true
|
|
151
|
+
delay 0.15
|
|
152
|
+
if name of front window does not contain slug then error "Wrong front window"
|
|
153
|
+
{body}
|
|
154
|
+
end tell
|
|
155
|
+
end tell
|
|
156
|
+
'''
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
['osascript', '-e', script], check=True,
|
|
159
|
+
capture_output=capture_output, text=True
|
|
160
|
+
)
|
|
161
|
+
return result.stdout.strip() if capture_output else ''
|
|
162
|
+
|
|
163
|
+
def wait_for_window(self, timeout=30):
|
|
164
|
+
deadline = time.monotonic() + timeout
|
|
165
|
+
while time.monotonic() < deadline:
|
|
166
|
+
try:
|
|
167
|
+
self.targeted('return name of front window', True)
|
|
168
|
+
return
|
|
169
|
+
except subprocess.CalledProcessError:
|
|
170
|
+
time.sleep(0.4)
|
|
171
|
+
raise RuntimeError('VS Code window did not appear')
|
|
172
|
+
|
|
173
|
+
def assert_window_size(self, minimum_size):
|
|
174
|
+
raw_size = self.targeted(
|
|
175
|
+
'set s to size of front window\n'
|
|
176
|
+
'return (item 1 of s as text) & "," & (item 2 of s as text)',
|
|
177
|
+
True
|
|
178
|
+
)
|
|
179
|
+
width, height = [int(value) for value in raw_size.split(',')]
|
|
180
|
+
if width < minimum_size[0] or height < minimum_size[1]:
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
f'VS Code window is {width}x{height}; minimum is '
|
|
183
|
+
f'{minimum_size[0]}x{minimum_size[1]}'
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def window_geometry(self):
|
|
187
|
+
raw_geometry = self.targeted(
|
|
188
|
+
'set p to position of front window\n'
|
|
189
|
+
'set s to size of front window\n'
|
|
190
|
+
'return (item 1 of p as text) & "," & '
|
|
191
|
+
'(item 2 of p as text) & "," & '
|
|
192
|
+
'(item 1 of s as text) & "," & (item 2 of s as text)',
|
|
193
|
+
True
|
|
194
|
+
)
|
|
195
|
+
return tuple(int(value) for value in raw_geometry.split(','))
|
|
196
|
+
|
|
197
|
+
def open_terminal(self):
|
|
198
|
+
self.targeted(
|
|
199
|
+
'click menu item "New Terminal" of menu 1 of '
|
|
200
|
+
'menu bar item "Terminal" of menu bar 1'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def drag(self, start, end):
|
|
204
|
+
self.targeted('return name of front window', True)
|
|
205
|
+
subprocess.run([
|
|
206
|
+
str(self.drag_helper), str(start[0]), str(start[1]),
|
|
207
|
+
str(end[0]), str(end[1])
|
|
208
|
+
], check=True)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _column_pixels(image_path, x, height):
|
|
212
|
+
result = subprocess.run([
|
|
213
|
+
'magick', str(image_path), '-crop', f'1x{height}+{x}+0',
|
|
214
|
+
'-depth', '8', 'txt:-'
|
|
215
|
+
], check=True, capture_output=True, text=True)
|
|
216
|
+
pattern = re.compile(r'^0,(\d+): \((\d+),(\d+),(\d+)')
|
|
217
|
+
pixels = []
|
|
218
|
+
for line in result.stdout.splitlines():
|
|
219
|
+
match = pattern.match(line)
|
|
220
|
+
if match:
|
|
221
|
+
pixels.append(tuple(int(value) for value in match.groups()[1:]))
|
|
222
|
+
if len(pixels) != height:
|
|
223
|
+
raise RuntimeError(
|
|
224
|
+
f'could not read screenshot column at x={x}: '
|
|
225
|
+
f'expected {height} pixels, found {len(pixels)}'
|
|
226
|
+
)
|
|
227
|
+
return pixels
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _average_color(pixels, start, end):
|
|
231
|
+
count = end - start
|
|
232
|
+
return tuple(
|
|
233
|
+
sum(pixels[index][channel] for index in range(start, end)) / count
|
|
234
|
+
for channel in range(3)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def detect_terminal_divider(self, screenshot_path, search_range):
|
|
238
|
+
window_x, window_y, window_width, window_height = self.window_geometry()
|
|
239
|
+
dimensions = subprocess.run([
|
|
240
|
+
'magick', 'identify', '-format', '%w,%h', str(screenshot_path)
|
|
241
|
+
], check=True, capture_output=True, text=True).stdout.strip()
|
|
242
|
+
image_width, image_height = [int(value) for value in dimensions.split(',')]
|
|
243
|
+
logical_width, logical_height = self.logical_display_size()
|
|
244
|
+
scale_x = image_width / logical_width
|
|
245
|
+
scale_y = image_height / logical_height
|
|
246
|
+
if abs(scale_x - scale_y) > 0.01:
|
|
247
|
+
raise RuntimeError('display screenshot has inconsistent pixel scaling')
|
|
248
|
+
scale = scale_x
|
|
249
|
+
|
|
250
|
+
logical_xs = [
|
|
251
|
+
window_x + round(window_width * fraction)
|
|
252
|
+
for fraction in (0.20, 0.35, 0.55)
|
|
253
|
+
]
|
|
254
|
+
columns = [
|
|
255
|
+
self._column_pixels(
|
|
256
|
+
screenshot_path, round(logical_x * scale), image_height
|
|
257
|
+
)
|
|
258
|
+
for logical_x in logical_xs
|
|
259
|
+
]
|
|
260
|
+
first_y = max(window_y + search_range[0], window_y + 80)
|
|
261
|
+
last_y = min(
|
|
262
|
+
window_y + search_range[1], window_y + window_height - 100
|
|
263
|
+
)
|
|
264
|
+
start = max(round(first_y * scale), 16)
|
|
265
|
+
end = min(round(last_y * scale), image_height - 16)
|
|
266
|
+
|
|
267
|
+
best = None
|
|
268
|
+
for y in range(start, end):
|
|
269
|
+
column_scores = []
|
|
270
|
+
for pixels in columns:
|
|
271
|
+
above = self._average_color(pixels, y - 12, y - 4)
|
|
272
|
+
below = self._average_color(pixels, y + 4, y + 12)
|
|
273
|
+
distance = sum(
|
|
274
|
+
(above[channel] - below[channel]) ** 2
|
|
275
|
+
for channel in range(3)
|
|
276
|
+
) ** 0.5
|
|
277
|
+
column_scores.append(distance)
|
|
278
|
+
score = median(column_scores)
|
|
279
|
+
if min(column_scores) < score * 0.6:
|
|
280
|
+
continue
|
|
281
|
+
if best is None or score > best[0]:
|
|
282
|
+
best = (score, y)
|
|
283
|
+
|
|
284
|
+
if best is None or best[0] < 6:
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
'could not locate the horizontal terminal divider with confidence'
|
|
287
|
+
)
|
|
288
|
+
return round(best[1] / scale)
|
|
289
|
+
|
|
290
|
+
def resize_terminal_panel(self, x, target_y, search_range=(90, 950)):
|
|
291
|
+
if self.temp_root is None:
|
|
292
|
+
raise RuntimeError('disposable project has not been created')
|
|
293
|
+
before = self.temp_root / 'terminal-before.png'
|
|
294
|
+
after = self.temp_root / 'terminal-after.png'
|
|
295
|
+
self.targeted('return name of front window', True)
|
|
296
|
+
subprocess.run([
|
|
297
|
+
'/usr/sbin/screencapture', '-x', f'-D{self.display_number}', str(before)
|
|
298
|
+
], check=True)
|
|
299
|
+
handle_offset = 5
|
|
300
|
+
start_y = (
|
|
301
|
+
self.detect_terminal_divider(before, search_range) + handle_offset
|
|
302
|
+
)
|
|
303
|
+
if abs(start_y - target_y) > 4:
|
|
304
|
+
self.drag((x, start_y), (x, target_y))
|
|
305
|
+
time.sleep(0.5)
|
|
306
|
+
subprocess.run([
|
|
307
|
+
'/usr/sbin/screencapture', '-x', f'-D{self.display_number}', str(after)
|
|
308
|
+
], check=True)
|
|
309
|
+
actual_y = (
|
|
310
|
+
self.detect_terminal_divider(after, search_range) + handle_offset
|
|
311
|
+
)
|
|
312
|
+
if abs(actual_y - target_y) > 8:
|
|
313
|
+
raise RuntimeError(
|
|
314
|
+
f'terminal divider verification failed: expected y={target_y}, '
|
|
315
|
+
f'found y={actual_y} (started at y={start_y})'
|
|
316
|
+
)
|
|
317
|
+
before.unlink(missing_ok=True)
|
|
318
|
+
after.unlink(missing_ok=True)
|
|
319
|
+
return start_y, actual_y
|
|
320
|
+
|
|
321
|
+
def prepare_default_vscode(self, profile=None):
|
|
322
|
+
selected = profile or DEFAULT_VSCODE_PROFILE
|
|
323
|
+
if self.logical_display_size() != tuple(selected['logical_display']):
|
|
324
|
+
raise RuntimeError('logical display does not match the accepted profile')
|
|
325
|
+
self.create_copy()
|
|
326
|
+
self.open_vscode()
|
|
327
|
+
self.wait_for_window()
|
|
328
|
+
self.assert_window_size(selected['minimum_window_size'])
|
|
329
|
+
auxiliary_panel_key_code = selected.get('auxiliary_panel_key_code')
|
|
330
|
+
if auxiliary_panel_key_code is not None:
|
|
331
|
+
self.hotkey(auxiliary_panel_key_code)
|
|
332
|
+
time.sleep(0.4)
|
|
333
|
+
self.open_terminal()
|
|
334
|
+
time.sleep(selected.get('terminal_open_delay', 1.5))
|
|
335
|
+
self.resize_terminal_panel(
|
|
336
|
+
selected['terminal_drag_x'], selected['terminal_target_y'],
|
|
337
|
+
selected['terminal_search_y']
|
|
338
|
+
)
|
|
339
|
+
time.sleep(0.4)
|
|
340
|
+
|
|
341
|
+
def quick_open(self, relative_path):
|
|
342
|
+
path = self._apple_string(relative_path)
|
|
343
|
+
self.targeted(f'''
|
|
344
|
+
keystroke "p" using command down
|
|
345
|
+
delay 0.25
|
|
346
|
+
set savedClipboard to missing value
|
|
347
|
+
try
|
|
348
|
+
set savedClipboard to the clipboard as record
|
|
349
|
+
end try
|
|
350
|
+
set the clipboard to "{path}"
|
|
351
|
+
keystroke "v" using command down
|
|
352
|
+
delay 0.2
|
|
353
|
+
key code 36
|
|
354
|
+
if savedClipboard is not missing value then set the clipboard to savedClipboard
|
|
355
|
+
''')
|
|
356
|
+
|
|
357
|
+
def hotkey(self, key_code, modifiers='command down'):
|
|
358
|
+
self.targeted(f'key code {key_code} using {modifiers}')
|
|
359
|
+
|
|
360
|
+
def type_text(self, text, character_delay=0.045):
|
|
361
|
+
safe = self._apple_string(text)
|
|
362
|
+
self.targeted(f'''
|
|
363
|
+
repeat with currentCharacter in characters of "{safe}"
|
|
364
|
+
keystroke currentCharacter
|
|
365
|
+
delay {character_delay}
|
|
366
|
+
end repeat
|
|
367
|
+
''')
|
|
368
|
+
|
|
369
|
+
def wait_for_files(self, relative_paths, timeout=60):
|
|
370
|
+
deadline = time.monotonic() + timeout
|
|
371
|
+
while time.monotonic() < deadline:
|
|
372
|
+
if all((self.project / path).exists() for path in relative_paths):
|
|
373
|
+
return
|
|
374
|
+
time.sleep(0.2)
|
|
375
|
+
missing = [path for path in relative_paths if not (self.project / path).exists()]
|
|
376
|
+
raise RuntimeError('timed out waiting for: ' + ', '.join(missing))
|
|
377
|
+
|
|
378
|
+
def mark(self, name, **details):
|
|
379
|
+
self.events.append({
|
|
380
|
+
'name': name,
|
|
381
|
+
'timestamp_ms': round(time.monotonic() * 1000),
|
|
382
|
+
**details
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
def start_capture(self):
|
|
386
|
+
self.output_video.parent.mkdir(parents=True, exist_ok=True)
|
|
387
|
+
self.capture = subprocess.Popen([
|
|
388
|
+
'/usr/sbin/screencapture', '-v', f'-D{self.display_number}', '-k', str(self.output_video)
|
|
389
|
+
])
|
|
390
|
+
self.mark('recording_started')
|
|
391
|
+
|
|
392
|
+
def stop_capture(self):
|
|
393
|
+
if self.capture is None:
|
|
394
|
+
return
|
|
395
|
+
if self.capture.poll() is None:
|
|
396
|
+
self.capture.send_signal(signal.SIGINT)
|
|
397
|
+
code = self.capture.wait(timeout=30)
|
|
398
|
+
self.capture = None
|
|
399
|
+
if code != 0 or not self.output_video.is_file():
|
|
400
|
+
raise RuntimeError(f'screen capture failed with exit code {code}')
|
|
401
|
+
self.mark('recording_stopped')
|
|
402
|
+
|
|
403
|
+
def write_events(self, metadata=None):
|
|
404
|
+
payload = {'schema_version': 1, **(metadata or {}), 'events': self.events}
|
|
405
|
+
self.output_events.write_text(json.dumps(payload, indent=2) + '\n')
|
|
406
|
+
|
|
407
|
+
def cleanup(self):
|
|
408
|
+
if self.capture is not None:
|
|
409
|
+
try:
|
|
410
|
+
self.stop_capture()
|
|
411
|
+
except Exception:
|
|
412
|
+
if self.capture is not None:
|
|
413
|
+
self.capture.kill()
|
|
414
|
+
self.capture.wait()
|
|
415
|
+
self.capture = None
|
|
416
|
+
if self.window_slug:
|
|
417
|
+
try:
|
|
418
|
+
self.targeted('keystroke "w" using {command down, shift down}')
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
if self.project:
|
|
422
|
+
time.sleep(0.8)
|
|
423
|
+
shutil.rmtree(self.project, ignore_errors=True)
|
|
424
|
+
time.sleep(0.4)
|
|
425
|
+
shutil.rmtree(self.project, ignore_errors=True)
|
|
426
|
+
if self.temp_root:
|
|
427
|
+
shutil.rmtree(self.temp_root, ignore_errors=True)
|