-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcairo0_rustlings.py
More file actions
415 lines (324 loc) · 12.6 KB
/
cairo0_rustlings.py
File metadata and controls
415 lines (324 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
#!/usr/bin/env python3
"""
Cairo0 Rustlings
A tool to help you learn Cairo0 by working through small exercises.
"""
import argparse
import os
import platform
import signal
import subprocess
import sys
import time
from enum import Enum
from pathlib import Path
from typing import Optional
import toml
from tabulate import tabulate
# ANSI color codes for terminal output
class Colors:
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
# Exercise status
class ExerciseStatus(Enum):
PENDING = "pending"
DONE = "done"
# Global state
EXERCISE_ORDER = []
EXERCISE_HINTS = {}
CURRENT_EXERCISE_INDEX = 0
EXERCISE_STATUS = {}
WATCH_MODE = True
MANUAL_RUN = False
def clear_screen():
"""Clear the terminal screen based on OS"""
os.system("cls" if platform.system() == "Windows" else "clear")
def print_header():
"""Print the Cairo0 Rustlings header"""
clear_screen()
print(f"{Colors.BOLD}{Colors.BLUE}Cairo0 Rustlings{Colors.ENDC}")
print(f"{Colors.CYAN}Learn Cairo0 by solving exercises!{Colors.ENDC}")
print("-----------------------------------------------")
def load_exercise_data():
"""Load exercise order and hints from TOML file"""
global EXERCISE_ORDER, EXERCISE_HINTS, EXERCISE_STATUS
# Load order and hints from TOML file
try:
data = toml.load("exercise_order.toml")
EXERCISE_ORDER = data.get("exercises", {}).get("order", [])
EXERCISE_HINTS = data.get("hints", {})
except Exception as e:
print(f"{Colors.RED}Error loading exercise order: {e}{Colors.ENDC}")
sys.exit(1)
# Initialize exercise status
for exercise in EXERCISE_ORDER:
EXERCISE_STATUS[exercise] = ExerciseStatus.PENDING
def is_exercise_done(exercise_path: str) -> bool:
"""Check if the I AM NOT DONE marker is removed from the file"""
cairo_file = Path("exercises") / f"{exercise_path}.cairo"
if not cairo_file.exists():
return False
content = cairo_file.read_text()
return "I AM NOT DONE" not in content
def update_exercise_status():
"""Update the status of all exercises"""
global EXERCISE_STATUS
for exercise in EXERCISE_ORDER:
if is_exercise_done(exercise):
EXERCISE_STATUS[exercise] = ExerciseStatus.DONE
else:
EXERCISE_STATUS[exercise] = ExerciseStatus.PENDING
def get_next_pending_exercise() -> Optional[str]:
"""Get the next pending exercise in the list"""
for exercise in EXERCISE_ORDER:
if EXERCISE_STATUS[exercise] == ExerciseStatus.PENDING:
return exercise
return None
def run_exercise_test(exercise_path: str) -> bool:
"""Run the test for a specific exercise using pytest"""
test_path = Path("exercises") / f"{exercise_path}.py"
if not test_path.exists():
print(f"{Colors.RED}Error: Test file not found: {test_path}{Colors.ENDC}")
return False
print(f"{Colors.BLUE}Running test for {exercise_path}...{Colors.ENDC}")
try:
# Run pytest with uv
result = subprocess.run(
["uv", "run", "pytest", str(test_path), "-v"],
capture_output=True,
text=True,
)
# Print the output
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr)
# Check if the test passed
return result.returncode == 0
except Exception as e:
print(f"{Colors.RED}Error running test: {e}{Colors.ENDC}")
return False
def get_exercise_hint(exercise_path: str) -> str:
"""Get hint for the current exercise"""
exercise_name = exercise_path.split("/")[-1]
hint = EXERCISE_HINTS.get(exercise_name, "No hint available for this exercise.")
return hint
def display_exercise_status(exercise_path: str):
"""Display the current status of the exercise"""
cairo_file = Path("exercises") / f"{exercise_path}.cairo"
test_file = Path("exercises") / f"{exercise_path}.py"
print(f"\n{Colors.BOLD}Current exercise: {Colors.BLUE}{exercise_path}{Colors.ENDC}")
if not cairo_file.exists():
print(f"{Colors.RED}Error: Cairo file not found: {cairo_file}{Colors.ENDC}")
return
if not test_file.exists():
print(f"{Colors.RED}Error: Test file not found: {test_file}{Colors.ENDC}")
return
# Show file status
status = EXERCISE_STATUS[exercise_path]
status_color = Colors.GREEN if status == ExerciseStatus.DONE else Colors.YELLOW
print(f"Status: {status_color}{status.value}{Colors.ENDC}")
# Run the test if the file is marked as done
if status == ExerciseStatus.DONE:
if run_exercise_test(exercise_path):
print(f"{Colors.GREEN}Exercise completed successfully!{Colors.ENDC}")
else:
print(f"{Colors.RED}Exercise test failed.{Colors.ENDC}")
else:
# Display the exercise content
content = cairo_file.read_text()
print("\nExercise file content:")
print(f"{Colors.CYAN}{'=' * 50}{Colors.ENDC}")
print(content)
print(f"{Colors.CYAN}{'=' * 50}{Colors.ENDC}")
# Show hint
print(f"\n{Colors.BOLD}Hint:{Colors.ENDC} {get_exercise_hint(exercise_path)}")
def watch_exercise_file(exercise_path: str):
"""Watch for changes in the exercise file"""
global MANUAL_RUN
cairo_file = Path("exercises") / f"{exercise_path}.cairo"
last_modified = cairo_file.stat().st_mtime if cairo_file.exists() else 0
while WATCH_MODE:
if not cairo_file.exists():
print(f"{Colors.RED}Error: File not found: {cairo_file}{Colors.ENDC}")
time.sleep(1)
continue
current_modified = cairo_file.stat().st_mtime
if current_modified > last_modified or MANUAL_RUN:
clear_screen()
print_header()
# Check if the exercise is now marked as done
if is_exercise_done(exercise_path):
EXERCISE_STATUS[exercise_path] = ExerciseStatus.DONE
display_exercise_status(exercise_path)
if EXERCISE_STATUS[exercise_path] == ExerciseStatus.DONE:
# If this exercise is done and all tests pass, check if there are more exercises
if run_exercise_test(exercise_path):
next_exercise = get_next_pending_exercise()
if next_exercise:
print(
f"\n{Colors.GREEN}Great job! Moving to the next exercise: {next_exercise}{Colors.ENDC}"
)
time.sleep(2)
return next_exercise
else:
print(
f"\n{Colors.GREEN}Congratulations! You've completed all exercises!{Colors.ENDC}"
)
return None
last_modified = current_modified
if MANUAL_RUN:
MANUAL_RUN = False
# Process keyboard commands
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
cmd = sys.stdin.readline().strip()
if cmd == "q":
return None # Exit watch mode
elif cmd == "h":
print(
f"\n{Colors.BOLD}Hint:{Colors.ENDC} {get_exercise_hint(exercise_path)}"
)
elif cmd == "r":
MANUAL_RUN = True
elif cmd == "l":
show_exercise_list()
time.sleep(0.5)
return None
def show_exercise_list():
"""Show an interactive list of all exercises"""
global CURRENT_EXERCISE_INDEX
update_exercise_status()
while True:
clear_screen()
print_header()
# Create a table of exercises
table_data = []
for i, exercise in enumerate(EXERCISE_ORDER):
status = EXERCISE_STATUS[exercise]
status_str = "✓" if status == ExerciseStatus.DONE else " "
current = ">" if i == CURRENT_EXERCISE_INDEX else " "
table_data.append([current, f"{i+1}", status_str, exercise])
# Print the table
print(
tabulate(
table_data, headers=["", "#", "Done", "Exercise"], tablefmt="simple"
)
)
print("\nCommands:")
print(" c: Continue selected exercise")
print(" p/n: Previous/Next exercise")
print(" r: Reset exercise status")
print(" q: Return to watch mode")
# Get user input
key = input("\nEnter command: ").strip().lower()
if key == "c":
return EXERCISE_ORDER[CURRENT_EXERCISE_INDEX]
elif key == "p" and CURRENT_EXERCISE_INDEX > 0:
CURRENT_EXERCISE_INDEX -= 1
elif key == "n" and CURRENT_EXERCISE_INDEX < len(EXERCISE_ORDER) - 1:
CURRENT_EXERCISE_INDEX += 1
elif key == "r":
# Reset the selected exercise
exercise = EXERCISE_ORDER[CURRENT_EXERCISE_INDEX]
cairo_file = Path("exercises") / f"{exercise}.cairo"
if cairo_file.exists():
content = cairo_file.read_text()
if "I AM NOT DONE" not in content:
# Find the comment line index
lines = content.split("\n")
for i, line in enumerate(lines):
if line.strip().startswith(
"//"
) and not line.strip().startswith("// I AM NOT DONE"):
lines.insert(i, "// I AM NOT DONE")
break
# Write the modified content back
cairo_file.write_text("\n".join(lines))
EXERCISE_STATUS[exercise] = ExerciseStatus.PENDING
elif key == "q":
break
return EXERCISE_ORDER[CURRENT_EXERCISE_INDEX]
def run_watch_mode():
"""Run the watch mode, monitoring files and running tests"""
global CURRENT_EXERCISE_INDEX, WATCH_MODE
print_header()
print(f"{Colors.CYAN}Watch mode enabled. Press Ctrl+C to exit.{Colors.ENDC}")
print("Commands: h (hint), r (run test), l (list exercises), q (quit)")
# Set up signal handler for Ctrl+C
def signal_handler(sig, frame):
global WATCH_MODE
WATCH_MODE = False
print(f"\n{Colors.YELLOW}Exiting watch mode...{Colors.ENDC}")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
# Find the first pending exercise
current_exercise = get_next_pending_exercise()
if not current_exercise:
print(f"{Colors.GREEN}All exercises are completed!{Colors.ENDC}")
return
# Set the current exercise index
CURRENT_EXERCISE_INDEX = EXERCISE_ORDER.index(current_exercise)
# Start watching for changes
while WATCH_MODE and current_exercise:
display_exercise_status(current_exercise)
next_exercise = watch_exercise_file(current_exercise)
if next_exercise:
current_exercise = next_exercise
CURRENT_EXERCISE_INDEX = EXERCISE_ORDER.index(current_exercise)
else:
break
def init_command():
"""Initialize the environment by creating necessary files"""
# Create exercise_order.toml if it doesn't exist
if not Path("exercise_order.toml").exists():
print(f"{Colors.YELLOW}Creating exercise_order.toml...{Colors.ENDC}")
# We've already created this in our script, so we'll just print a message here
print(f"{Colors.GREEN}Cairo0 Rustlings initialized successfully!{Colors.ENDC}")
print(
f"Run '{Colors.BOLD}uv run python cairo0_rustlings.py{Colors.ENDC}' to start the exercises."
)
def main():
"""Main entry point"""
global WATCH_MODE, MANUAL_RUN
parser = argparse.ArgumentParser(
description="Cairo0 Rustlings - Learn Cairo by solving exercises"
)
parser.add_argument(
"command",
nargs="?",
default="watch",
choices=["watch", "init", "list"],
help="Command to run (watch, init, list)",
)
parser.add_argument(
"--manual-run",
action="store_true",
help="Manually run tests (don't watch for file changes)",
)
args = parser.parse_args()
# Import select only if we're in watch mode
if args.command == "watch":
global select
import select
if args.manual_run:
MANUAL_RUN = True
if args.command == "init":
init_command()
return
# Load exercise data
load_exercise_data()
update_exercise_status()
if args.command == "list":
show_exercise_list()
return
# Run watch mode
run_watch_mode()
if __name__ == "__main__":
main()