chefmate 1.0.0__py3-none-any.whl
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.
- chefmate/__init__.py +0 -0
- chefmate/browser_manager.py +70 -0
- chefmate/chefmate_core.py +1536 -0
- chefmate/cli.py +275 -0
- chefmate/config.py +64 -0
- chefmate-1.0.0.dist-info/METADATA +313 -0
- chefmate-1.0.0.dist-info/RECORD +11 -0
- chefmate-1.0.0.dist-info/WHEEL +5 -0
- chefmate-1.0.0.dist-info/entry_points.txt +2 -0
- chefmate-1.0.0.dist-info/licenses/LICENSE +21 -0
- chefmate-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1536 @@
|
|
1
|
+
# ------------------------------------------------------------ Welcome to ChefMate --------------------------------------------------------
|
2
|
+
|
3
|
+
# ---------------- Necessary Imports -------------------------------
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
import time
|
7
|
+
import click
|
8
|
+
import platform
|
9
|
+
import colorama
|
10
|
+
import inquirer
|
11
|
+
import traceback
|
12
|
+
import subprocess
|
13
|
+
import pandas as pd
|
14
|
+
from rich import box
|
15
|
+
from rich.text import Text
|
16
|
+
from rich.table import Table
|
17
|
+
from rich.panel import Panel
|
18
|
+
from rich.align import Align
|
19
|
+
from rich.console import Console
|
20
|
+
from rich.progress import Progress
|
21
|
+
from tabulate import tabulate
|
22
|
+
from colorama import Fore, Style
|
23
|
+
from InquirerPy import inquirer
|
24
|
+
from chefmate.config import Config
|
25
|
+
from InquirerPy.base.control import Choice
|
26
|
+
from selenium.webdriver.common.by import By
|
27
|
+
from selenium.webdriver.common.keys import Keys
|
28
|
+
from chefmate.browser_manager import BrowserManager
|
29
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
30
|
+
from selenium.webdriver.support import expected_conditions as EC
|
31
|
+
# ------------------------------------------------------------------
|
32
|
+
|
33
|
+
# Initialize colorama
|
34
|
+
colorama.init(autoreset=True)
|
35
|
+
console = Console()
|
36
|
+
|
37
|
+
# Language mapping
|
38
|
+
LANGUAGE_VALUES = {
|
39
|
+
"C++": "C++", "Python3": "PYTH 3", "Python 3": "PYTH 3",
|
40
|
+
"C": "C", "Java": "JAVA", "PyPy3": "PYPY3", "C#": "C#",
|
41
|
+
"JavaScript": "NODEJS", "Go": "GO", "TypeScript": "TS",
|
42
|
+
"PHP": "PHP", "Kotlin": "KTLN", "Rust": "rust", "R": "R"
|
43
|
+
}
|
44
|
+
|
45
|
+
# Language extensions mapping
|
46
|
+
LANGUAGE_EXTENSIONS = {
|
47
|
+
"C++": ".cpp", "Python3": ".py", "Python 3": ".py",
|
48
|
+
"C": ".c", "Java": ".java", "PyPy3": ".py", "C#": ".cs",
|
49
|
+
"JavaScript": ".js", "Go": ".go", "TypeScript": ".ts",
|
50
|
+
"PHP": ".php", "Kotlin": ".kt", "Rust": ".rs", "R": ".r"
|
51
|
+
}
|
52
|
+
|
53
|
+
# Default paths
|
54
|
+
DEFAULT_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".chefmate")
|
55
|
+
DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, "config.json")
|
56
|
+
|
57
|
+
class ChefMate:
|
58
|
+
"""Main ChefMate class for CodeChef automation"""
|
59
|
+
|
60
|
+
def hide_cursor(self):
|
61
|
+
sys.stdout.write("\033[?25l")
|
62
|
+
sys.stdout.flush()
|
63
|
+
|
64
|
+
def show_cursor(self):
|
65
|
+
sys.stdout.write("\033[?25h")
|
66
|
+
sys.stdout.flush()
|
67
|
+
|
68
|
+
def render_problem_tracker(self):
|
69
|
+
table = Table(
|
70
|
+
show_header=True,
|
71
|
+
header_style="bold magenta",
|
72
|
+
box=box.SIMPLE_HEAVY,
|
73
|
+
expand=False, # Prevent full-width stretching
|
74
|
+
)
|
75
|
+
|
76
|
+
table.add_column("Status", justify="center", width=7)
|
77
|
+
table.add_column("Problem", justify="left", style="bold")
|
78
|
+
|
79
|
+
symbol = {"pending": "🟡", "done": "✅", "wrong": "❌"}
|
80
|
+
|
81
|
+
for code, status in self.problem_status.items():
|
82
|
+
emoji = symbol.get(status, "❔")
|
83
|
+
table.add_row(emoji, code)
|
84
|
+
|
85
|
+
# Center the table itself
|
86
|
+
centered_table = Align.center(table, vertical="middle")
|
87
|
+
|
88
|
+
# Now return the panel with a fixed width
|
89
|
+
panel = Panel(centered_table, title="Problem Tracker", border_style="bold white", width=30)
|
90
|
+
return panel
|
91
|
+
|
92
|
+
def show_tracker(self):
|
93
|
+
print()
|
94
|
+
console.print(self.render_problem_tracker())
|
95
|
+
print()
|
96
|
+
|
97
|
+
def flush_stdin(self):
|
98
|
+
if platform.system() != "Windows":
|
99
|
+
import termios
|
100
|
+
try:
|
101
|
+
termios.tcflush(sys.stdin, termios.TCIFLUSH)
|
102
|
+
except:
|
103
|
+
pass
|
104
|
+
else:
|
105
|
+
import msvcrt
|
106
|
+
try:
|
107
|
+
while msvcrt.kbhit():
|
108
|
+
msvcrt.getch()
|
109
|
+
except:
|
110
|
+
pass
|
111
|
+
|
112
|
+
def ask(self, text):
|
113
|
+
return click.prompt(f" {Fore.MAGENTA}{text}")
|
114
|
+
|
115
|
+
def type_writer(self, text):
|
116
|
+
try:
|
117
|
+
self.hide_cursor()
|
118
|
+
for char in text:
|
119
|
+
sys.stdout.write(Fore.YELLOW + char)
|
120
|
+
sys.stdout.flush()
|
121
|
+
time.sleep(0.04)
|
122
|
+
sys.stdout.write("\n")
|
123
|
+
finally: self.show_cursor()
|
124
|
+
|
125
|
+
def goto_dashboard(self):
|
126
|
+
self.browser.driver.get("https://www.codechef.com/dashboard")
|
127
|
+
WebDriverWait(self.browser.driver, 10).until(EC.presence_of_element_located((By.XPATH, "//img[contains(@alt, 'CodeChef Logo')]")))
|
128
|
+
print(f"{Fore.GREEN} Dashboard opened successfully! \u2714\n")
|
129
|
+
|
130
|
+
def display_logo(self):
|
131
|
+
banner = Text()
|
132
|
+
logo = f"""
|
133
|
+
{Fore.RED}
|
134
|
+
╔═══════════════════════════════════════════════════════════════════════════════════╗
|
135
|
+
║ ║
|
136
|
+
║ ██████╗ ██╗ ██╗ ███████╗ ███████╗ ███╗ ███╗ █████╗ ████████╗ ███████╗ ║
|
137
|
+
║ ██╔════╝ ██║ ██║ ██╔════╝ ██╔════╝ ████╗ ████║ ██╔══██╗ ╚══██╔══╝ ██╔════╝ ║
|
138
|
+
║ ██║ ███████║ █████╗ █████╗ ██╔████╔██║ ███████║ ██║ █████╗ ║
|
139
|
+
║ ██║ ██╔══██║ ██╔══╝ ██╔══╝ ██║╚██╔╝██║ ██╔══██║ ██║ ██╔══╝ ║
|
140
|
+
║ ╚██████╗ ██║ ██║ ███████╗ ██║ ██║ ╚═╝ ██║ ██║ ██║ ██║ ███████╗ ║
|
141
|
+
║ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ║
|
142
|
+
║ ║
|
143
|
+
║═══════════════════════════════════════════════════════════════════════════════════║
|
144
|
+
║ ║
|
145
|
+
╚═══════════════════════════════════════════════════════════════════════════════════╝
|
146
|
+
{Style.RESET_ALL}
|
147
|
+
"""
|
148
|
+
|
149
|
+
banner.append(logo)
|
150
|
+
# Print logo immediately
|
151
|
+
console.print(banner)
|
152
|
+
|
153
|
+
# Typewriter effect for the last line
|
154
|
+
tagline = "Automation tool for CodeChef"
|
155
|
+
up_lines, right_spaces = 4, 28
|
156
|
+
|
157
|
+
sys.stdout.write(f"\033[{up_lines}A")
|
158
|
+
sys.stdout.write(f"\033[{right_spaces}C")
|
159
|
+
|
160
|
+
try:
|
161
|
+
self.hide_cursor()
|
162
|
+
for char in tagline:
|
163
|
+
sys.stdout.write(Fore.RED + char + Style.RESET_ALL)
|
164
|
+
sys.stdout.flush()
|
165
|
+
time.sleep(0.04)
|
166
|
+
|
167
|
+
sys.stdout.flush()
|
168
|
+
print('\n' * 2)
|
169
|
+
finally: self.show_cursor()
|
170
|
+
|
171
|
+
def __init__(self, config_file=DEFAULT_CONFIG_FILE):
|
172
|
+
self.config = Config(config_file)
|
173
|
+
self.browser = BrowserManager(self.config)
|
174
|
+
self.problems, self.tabs = [], []
|
175
|
+
self.contest_id = None # Add this line to store the contest ID
|
176
|
+
self.contest_dir = None # Add this line to store the contest directory
|
177
|
+
self.problem_status = {}
|
178
|
+
|
179
|
+
def setup(self):
|
180
|
+
"""Setup ChefMate configuration"""
|
181
|
+
username = click.prompt(f"{Fore.MAGENTA} Username", default=self.config.get("username", ""))
|
182
|
+
password = click.prompt(f"{Fore.MAGENTA} Password", hide_input=True, default=self.config.get("password", ""))
|
183
|
+
solution_path = click.prompt(f"{Fore.MAGENTA} Default solution file path", default=self.config.get("solution_path", ""))
|
184
|
+
chrome_user_data_dir = click.prompt(f"{Fore.MAGENTA} Chrome user data directory (optional)", default=self.config.get("chrome_user_data_dir", ""))
|
185
|
+
chrome_profile = click.prompt(f"{Fore.MAGENTA} Chrome profile name (e.g., Default, Profile 1, etc.) (optional)", default=self.config.get("chrome_profile", "Default"))
|
186
|
+
|
187
|
+
# Language Selections Here ------------------------------
|
188
|
+
lang_list = [
|
189
|
+
Choice(value="C++", name="C++"), Choice(value="Python 3", name="PYTH 3"), Choice(value="JAVA", name="Java"),
|
190
|
+
Choice(value="C", name="C"), Choice(value="PYPY3", name="PyPy3"), Choice(value="NODEJS", name="JavaScript"),
|
191
|
+
Choice(value="C#", name="C#"), Choice(value="GO", name="Go"), Choice(value="PHP", name="PHP"),
|
192
|
+
Choice(value="KTLN", name="Kotlin"), Choice(value="rust", name="Rust"),
|
193
|
+
Choice(value="R", name="R"), Choice(value="TS", name="TypeScript")
|
194
|
+
]
|
195
|
+
default_lang = "C++"
|
196
|
+
|
197
|
+
lang_selector = inquirer.select(
|
198
|
+
message="Choose your preferred language: ",
|
199
|
+
choices=lang_list,
|
200
|
+
default=default_lang, # Use the stored current choice
|
201
|
+
qmark="", # Remove the default [?]
|
202
|
+
pointer=">", # Custom arrow pointer
|
203
|
+
instruction="(Use arrow keys to navigate)"
|
204
|
+
).execute()
|
205
|
+
|
206
|
+
default_lang = lang_selector
|
207
|
+
print("\033[F\033[2K", end="")
|
208
|
+
print(f" You Chose: {Fore.CYAN}{default_lang}\n")
|
209
|
+
|
210
|
+
opt_list = [
|
211
|
+
Choice(value="default", name="Notepad (Default)"),
|
212
|
+
Choice(value="vscode", name="VS Code"),
|
213
|
+
Choice(value="sublime", name="Sublime Text"),
|
214
|
+
Choice(value="notepad++", name="Notepad++ (Windows)"),
|
215
|
+
Choice(value="atom", name="Atom"),
|
216
|
+
]
|
217
|
+
|
218
|
+
editor = "default"
|
219
|
+
|
220
|
+
choice = inquirer.select(
|
221
|
+
message="Choose your preferred editor installed in your system: ",
|
222
|
+
choices=opt_list,
|
223
|
+
default=editor, # Use the stored current choice
|
224
|
+
qmark="", # Remove the default [?]
|
225
|
+
pointer=">", # Custom arrow pointer
|
226
|
+
instruction="(Use arrow keys to navigate)"
|
227
|
+
).execute()
|
228
|
+
|
229
|
+
editor = choice
|
230
|
+
|
231
|
+
print("\033[F\033[2K", end="")
|
232
|
+
selected_opt = next((c.name for c in opt_list if c.value == choice), choice)
|
233
|
+
print(f" You Chose: {Fore.CYAN}{selected_opt}\n")
|
234
|
+
|
235
|
+
if choice == 1: editor = "default"
|
236
|
+
elif choice == 2: editor = "vscode"
|
237
|
+
elif choice == 3: editor = "sublime"
|
238
|
+
elif choice == 4: editor = "notepad++"
|
239
|
+
elif choice == 5: editor = "atom"
|
240
|
+
|
241
|
+
self.config.update_config(
|
242
|
+
username=username,
|
243
|
+
password=password,
|
244
|
+
preferred_language=default_lang,
|
245
|
+
solution_path=solution_path,
|
246
|
+
chrome_user_data_dir=chrome_user_data_dir,
|
247
|
+
preferred_editor=editor,
|
248
|
+
chrome_profile=chrome_profile,
|
249
|
+
)
|
250
|
+
|
251
|
+
print(f"{Fore.GREEN} Configuration saved successfully! \u2714\n")
|
252
|
+
|
253
|
+
def reconfig(self):
|
254
|
+
"""Reconfigure ChefMate"""
|
255
|
+
|
256
|
+
choice = 1
|
257
|
+
|
258
|
+
change_list = [
|
259
|
+
Choice(value=1, name="Everything"),
|
260
|
+
Choice(value=2, name="Username"),
|
261
|
+
Choice(value=3, name="Password"),
|
262
|
+
Choice(value=4, name="Preferred Language"),
|
263
|
+
Choice(value=5, name="Solution File Path"),
|
264
|
+
Choice(value=6, name="Chrome User Data Directory"),
|
265
|
+
Choice(value=7, name="Preferred Editor"),
|
266
|
+
Choice(value=8, name="Chrome Profile"),
|
267
|
+
]
|
268
|
+
|
269
|
+
choice_selector = inquirer.select(
|
270
|
+
message="What Changes do you want to make: ",
|
271
|
+
choices=change_list,
|
272
|
+
default=choice, # Use the stored current choice
|
273
|
+
qmark="", # Remove the default [?]
|
274
|
+
pointer=">", # Custom arrow pointer
|
275
|
+
instruction="(Use arrow keys to navigate)"
|
276
|
+
).execute()
|
277
|
+
|
278
|
+
choice = choice_selector
|
279
|
+
print("\033[F\033[2K", end="")
|
280
|
+
selected_opt = next((c.name for c in change_list if c.value == choice_selector), choice_selector)
|
281
|
+
print(f" You Chose: {Fore.CYAN}{selected_opt}\n")
|
282
|
+
|
283
|
+
if choice == 1: self.setup()
|
284
|
+
elif choice == 2: self.config.set("username", click.prompt(f"{Fore.MAGENTA} New Username", default=self.config.get("username", "")))
|
285
|
+
elif choice == 3: self.config.set("password", click.prompt(f"{Fore.MAGENTA} New Password", hide_input=True, default=self.config.get("password", "")))
|
286
|
+
elif choice == 4:
|
287
|
+
lang_list = [
|
288
|
+
Choice(value="C++", name="C++"), Choice(value="Python 3", name="PYTH 3"), Choice(value="JAVA", name="Java"),
|
289
|
+
Choice(value="C", name="C"), Choice(value="PYPY3", name="PyPy3"), Choice(value="NODEJS", name="JavaScript"),
|
290
|
+
Choice(value="C#", name="C#"), Choice(value="GO", name="Go"), Choice(value="PHP", name="PHP"),
|
291
|
+
Choice(value="KTLN", name="Kotlin"), Choice(value="rust", name="Rust"),
|
292
|
+
Choice(value="R", name="R"), Choice(value="TS", name="TypeScript")
|
293
|
+
]
|
294
|
+
default_lang = "C++"
|
295
|
+
|
296
|
+
lang_selector = inquirer.select(
|
297
|
+
message="Choose your preferred language: ",
|
298
|
+
choices=lang_list,
|
299
|
+
default=default_lang, # Use the stored current choice
|
300
|
+
qmark="", # Remove the default [?]
|
301
|
+
pointer=">", # Custom arrow pointer
|
302
|
+
instruction="(Use arrow keys to navigate)"
|
303
|
+
).execute()
|
304
|
+
|
305
|
+
print("\033[F\033[2K", end="")
|
306
|
+
print(f" You Chose: {Fore.CYAN}{lang_selector}\n")
|
307
|
+
|
308
|
+
self.config.set("preferred_language", lang_selector)
|
309
|
+
elif choice == 5: self.config.set("solution_path", click.prompt(f"{Fore.MAGENTA} New Solution File Path", default=self.config.get("solution_path", "")))
|
310
|
+
elif choice == 6: self.config.set("chrome_user_data_dir", click.prompt(f"{Fore.MAGENTA} New Chrome User Data Directory", default=self.config.get("chrome_user_data_dir", "")))
|
311
|
+
elif choice == 7:
|
312
|
+
editors_list = [
|
313
|
+
Choice(value="default", name="Notepad (System Default)"),
|
314
|
+
Choice(value="vscode", name="VS Code"),
|
315
|
+
Choice(value="sublime", name="Sublime Text"),
|
316
|
+
Choice(value="notepad++", name="Notepad++ (Windows)"),
|
317
|
+
Choice(value="atom", name="Atom"),
|
318
|
+
]
|
319
|
+
|
320
|
+
editor = "default"
|
321
|
+
|
322
|
+
editor_selector = inquirer.select(
|
323
|
+
message="Choose your preferred editor installed in your system: ",
|
324
|
+
choices=editors_list,
|
325
|
+
default=editor, # Use the stored current choice
|
326
|
+
qmark="", # Remove the default [?]
|
327
|
+
pointer=">", # Custom arrow pointer
|
328
|
+
instruction="(Use arrow keys to navigate)"
|
329
|
+
).execute()
|
330
|
+
|
331
|
+
editor = editor_selector
|
332
|
+
print("\033[F\033[2K", end="")
|
333
|
+
selected_opt = next((c.name for c in editors_list if c.value == editor_selector), editor_selector)
|
334
|
+
print(f" You Chose: {Fore.CYAN}{selected_opt}")
|
335
|
+
|
336
|
+
self.config.set("preferred_editor", editor)
|
337
|
+
elif choice == 8:
|
338
|
+
self.config.set("chrome_profile", click.prompt(f"{Fore.MAGENTA} New profile name (e.g., Default, Profile 1, etc.) (optional)", default=self.config.get("chrome_profile", "Default")))
|
339
|
+
print(f"{Fore.GREEN} Configuration updated successfully! \u2714\n")
|
340
|
+
|
341
|
+
def initialize(self):
|
342
|
+
"""Initialize browser and start session"""
|
343
|
+
try:
|
344
|
+
return self.browser.initialize_driver()
|
345
|
+
except Exception as e:
|
346
|
+
print(f"{Fore.RED} Close all instances of chrome running in the background\n")
|
347
|
+
return False
|
348
|
+
|
349
|
+
# ------------------- Login and Logout Process -------------------
|
350
|
+
def already_logged_in(self) -> bool:
|
351
|
+
"""Check if already logged in to CodeChef"""
|
352
|
+
dashboard_URL = "https://www.codechef.com/dashboard"
|
353
|
+
self.browser.driver.get(dashboard_URL)
|
354
|
+
try:
|
355
|
+
WebDriverWait(self.browser.driver, 10).until(
|
356
|
+
EC.presence_of_element_located((By.XPATH, "//img[contains(@alt, 'CodeChef Logo') ]"))
|
357
|
+
)
|
358
|
+
return self.browser.driver.current_url == dashboard_URL
|
359
|
+
except: return False
|
360
|
+
|
361
|
+
def login(self, first_login: bool = False):
|
362
|
+
"""Login to CodeChef"""
|
363
|
+
if not self.browser.driver:
|
364
|
+
if not self.initialize():
|
365
|
+
print(f"{Fore.RED} Failed to initialize browser.\n")
|
366
|
+
return False
|
367
|
+
|
368
|
+
if self.already_logged_in():
|
369
|
+
print(f"{Fore.GREEN} Already logged in \u2714\n")
|
370
|
+
return True
|
371
|
+
|
372
|
+
username = self.config.get("username", "")
|
373
|
+
password = self.config.get("password", "")
|
374
|
+
|
375
|
+
if not username or not password:
|
376
|
+
username = self.ask("Username: ")
|
377
|
+
password = self.ask("Password: ")
|
378
|
+
save = click.confirm(" Save credentials for future use?", default=True)
|
379
|
+
if save:
|
380
|
+
self.config.update_config(username=username, password=password)
|
381
|
+
|
382
|
+
self.browser.driver.get("https://www.codechef.com/login")
|
383
|
+
self.type_writer(" Loggin in ... ")
|
384
|
+
|
385
|
+
WebDriverWait(self.browser.driver, 10).until(
|
386
|
+
EC.element_to_be_clickable((By.ID, "edit-name"))
|
387
|
+
)
|
388
|
+
|
389
|
+
if first_login:
|
390
|
+
self.browser.driver.refresh()
|
391
|
+
WebDriverWait(self.browser.driver, 10).until(
|
392
|
+
EC.element_to_be_clickable((By.ID, "edit-name"))
|
393
|
+
)
|
394
|
+
|
395
|
+
u_box = self.browser.driver.find_element(By.ID, "edit-name")
|
396
|
+
u_box.send_keys(Keys.CONTROL + "a" + Keys.BACKSPACE)
|
397
|
+
u_box.send_keys(username)
|
398
|
+
|
399
|
+
p_box = self.browser.driver.find_element(By.ID, "edit-pass")
|
400
|
+
p_box.send_keys(Keys.CONTROL + "a" + Keys.BACKSPACE)
|
401
|
+
p_box.send_keys(password + Keys.RETURN)
|
402
|
+
|
403
|
+
max_wait_time = 20
|
404
|
+
interval = 2
|
405
|
+
elapsed_time = 0
|
406
|
+
|
407
|
+
# For slow networks, retry login
|
408
|
+
while elapsed_time < max_wait_time:
|
409
|
+
try:
|
410
|
+
WebDriverWait(self.browser.driver, 5).until(
|
411
|
+
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, '_content__container')]")
|
412
|
+
))
|
413
|
+
print(f"{Fore.GREEN} Successfully loaded Dashboard \u2714\n{Style.RESET_ALL}")
|
414
|
+
return True
|
415
|
+
except:
|
416
|
+
elapsed_time += interval
|
417
|
+
print(f"{Fore.YELLOW} Waiting for Dashboard to load... {elapsed_time}/{max_wait_time} sec")
|
418
|
+
time.sleep(interval)
|
419
|
+
|
420
|
+
print(f"{Fore.RED} Login timed out. Please check your internet connection and try again.\n")
|
421
|
+
return False
|
422
|
+
|
423
|
+
def logout(self):
|
424
|
+
"""Logout from CodeChef"""
|
425
|
+
if not self.browser.driver:
|
426
|
+
print(f"{Fore.YELLOW} No active browser session.\n")
|
427
|
+
return
|
428
|
+
|
429
|
+
self.type_writer(" Logging out ... ")
|
430
|
+
|
431
|
+
self.browser.driver.get("https://www.codechef.com/logout")
|
432
|
+
try:
|
433
|
+
WebDriverWait(self.browser.driver, 10).until(
|
434
|
+
EC.presence_of_element_located((By.XPATH, "//img[contains(@alt, 'CodeChef Logo')]"))
|
435
|
+
)
|
436
|
+
print(f"{Fore.GREEN} Logged out successfully \u2714\n")
|
437
|
+
except:
|
438
|
+
print(f"{Fore.YELLOW} Logout status unclear, closing browser anyway.\n")
|
439
|
+
|
440
|
+
def quit(self):
|
441
|
+
"""Quit the browser and end session"""
|
442
|
+
if self.browser.driver:
|
443
|
+
self.browser.close()
|
444
|
+
print(f"{Fore.GREEN} Browser closed successfully! \u2714\n")
|
445
|
+
else:
|
446
|
+
print(f"{Fore.YELLOW} No active browser session to close.\n")
|
447
|
+
# ----------------------------------------------------------------
|
448
|
+
|
449
|
+
# ----------------- Problem Scraping, Opening in Tabs and Submitting Solutions --------
|
450
|
+
def open_contest(self):
|
451
|
+
"""
|
452
|
+
Prompts for contest ID and division, opens contest page and all problem tabs.
|
453
|
+
"""
|
454
|
+
|
455
|
+
if not self.already_logged_in():
|
456
|
+
self.login()
|
457
|
+
|
458
|
+
division_map = {1: 'A', 2: 'B', 3: 'C', 4: 'D'}
|
459
|
+
|
460
|
+
contest_id = click.prompt(f" {Fore.MAGENTA}Enter contest ID (e.g., 171)", type=int)
|
461
|
+
division = click.prompt(f" {Fore.MAGENTA}Enter your division (1-4)", type=click.Choice(['1', '2', '3', '4']))
|
462
|
+
division = int(division)
|
463
|
+
|
464
|
+
# Store the contest ID for future use
|
465
|
+
self.contest_id = str(contest_id)
|
466
|
+
|
467
|
+
contest_url = f"https://www.codechef.com/START{contest_id}{division_map[division]}"
|
468
|
+
print(f"{Fore.CYAN} Opening link: {contest_url}{Style.RESET_ALL}")
|
469
|
+
self.type_writer(" Opening contest page ... ")
|
470
|
+
|
471
|
+
self.browser.driver.get(contest_url)
|
472
|
+
try:
|
473
|
+
WebDriverWait(self.browser.driver, 10).until(
|
474
|
+
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'problem-table')]"))
|
475
|
+
)
|
476
|
+
except:
|
477
|
+
print(f"{Fore.YELLOW} Contest page looks different than expected, but continuing...")
|
478
|
+
|
479
|
+
base_url = "https://www.codechef.com"
|
480
|
+
self.problems = []
|
481
|
+
try:
|
482
|
+
table = WebDriverWait(self.browser.driver, 10).until(
|
483
|
+
EC.presence_of_element_located((By.CLASS_NAME, "dataTable"))
|
484
|
+
)
|
485
|
+
links = table.find_elements(By.TAG_NAME, "a")
|
486
|
+
except:
|
487
|
+
links = self.browser.driver.find_elements(By.XPATH, "//a[contains(@href, 'problems')]")
|
488
|
+
|
489
|
+
for link in links:
|
490
|
+
href = link.get_attribute('href')
|
491
|
+
if href and "/problems/" in href:
|
492
|
+
if not href.startswith(base_url):
|
493
|
+
href = base_url + href if href.startswith("/") else f"{base_url}/{href}"
|
494
|
+
code = href.split("/")[-1]
|
495
|
+
title = link.text.strip() or "Untitled Problem"
|
496
|
+
self.problems.append((code, title, href))
|
497
|
+
|
498
|
+
if not self.problems:
|
499
|
+
print(f"{Fore.RED} No problems found in this contest! \u2716")
|
500
|
+
return False
|
501
|
+
|
502
|
+
self.problem_status = {code: "pending" for code, _, _ in self.problems}
|
503
|
+
|
504
|
+
self.tabs = [self.browser.driver.current_window_handle]
|
505
|
+
with Progress() as progress:
|
506
|
+
task = progress.add_task("[yellow] Opening problem tabs", total=len(self.problems))
|
507
|
+
for _, _, url in self.problems:
|
508
|
+
self.browser.driver.execute_script("window.open(arguments[0]);", url)
|
509
|
+
WebDriverWait(self.browser.driver, 2).until(lambda d: len(d.window_handles) > len(self.tabs))
|
510
|
+
self.tabs.append(self.browser.driver.window_handles[-1])
|
511
|
+
progress.update(task, advance=1)
|
512
|
+
|
513
|
+
# Display problems
|
514
|
+
print(f"\n{Fore.GREEN} Found {len(self.problems)} problems:")
|
515
|
+
for idx, (code, title, _) in enumerate(self.problems, start=1):
|
516
|
+
print(f" {idx}. {code}: {title}")
|
517
|
+
|
518
|
+
print()
|
519
|
+
self.browser.driver.switch_to.window(self.tabs[0])
|
520
|
+
return True
|
521
|
+
|
522
|
+
def _get_solution_path(self):
|
523
|
+
"""Get and validate solution file path"""
|
524
|
+
solution_path = self.config.get("solution_path", "")
|
525
|
+
|
526
|
+
if not solution_path or not os.path.exists(solution_path):
|
527
|
+
solution_path = click.prompt("Enter the path to your solution file")
|
528
|
+
save_path = click.confirm("Save this path for future use?", default=True)
|
529
|
+
if save_path:
|
530
|
+
self.config.set("solution_path", solution_path)
|
531
|
+
|
532
|
+
if not os.path.exists(solution_path):
|
533
|
+
print(f"{Fore.RED}Solution file not found: {solution_path}")
|
534
|
+
return None
|
535
|
+
if not os.path.isfile(solution_path):
|
536
|
+
print(f"{Fore.RED}Solution path is not a file: {solution_path}")
|
537
|
+
return None
|
538
|
+
if not os.access(solution_path, os.R_OK):
|
539
|
+
print(f"{Fore.RED}Solution file is not readable: {solution_path}")
|
540
|
+
return None
|
541
|
+
if os.path.getsize(solution_path) == 0:
|
542
|
+
print(f"{Fore.RED}Solution file is empty: {solution_path}")
|
543
|
+
return None
|
544
|
+
if not solution_path.endswith(('.cpp', '.py', '.java', '.js', '.go', '.php', '.kt', '.rs', '.c++', '.cs', '.rb', '.r', '.c', '.ts')):
|
545
|
+
print(f"{Fore.RED}Unsupported file type: {solution_path}")
|
546
|
+
return None
|
547
|
+
|
548
|
+
return solution_path
|
549
|
+
|
550
|
+
def _select_language(self):
|
551
|
+
"""Select language in CodeChef UI"""
|
552
|
+
language = self.config.get("preferred_language", "")
|
553
|
+
|
554
|
+
if not language:
|
555
|
+
language = self.ask("Enter your preferred language (e.g., C++): ")
|
556
|
+
save_lang = click.confirm("Save this language preference?", default=True)
|
557
|
+
if save_lang:
|
558
|
+
self.config.set("preferred_language", language)
|
559
|
+
|
560
|
+
data_value = LANGUAGE_VALUES.get(language)
|
561
|
+
if not data_value:
|
562
|
+
print(f" {Fore.RED}Language {language} not supported.")
|
563
|
+
return False
|
564
|
+
|
565
|
+
try:
|
566
|
+
dropdown = WebDriverWait(self.browser.driver, 10).until(
|
567
|
+
EC.element_to_be_clickable((By.CSS_SELECTOR, "div[id^='language-select']"))
|
568
|
+
)
|
569
|
+
dropdown.click()
|
570
|
+
option = WebDriverWait(self.browser.driver, 10).until(
|
571
|
+
EC.element_to_be_clickable((By.CSS_SELECTOR, f"li[data-value='{data_value}']"))
|
572
|
+
)
|
573
|
+
option.click()
|
574
|
+
return True
|
575
|
+
except Exception as e:
|
576
|
+
print(f"{Fore.RED} Error selecting language: {e}")
|
577
|
+
return False
|
578
|
+
|
579
|
+
def _load_code(self, code_text):
|
580
|
+
"""Load code into editor"""
|
581
|
+
try:
|
582
|
+
text_input = WebDriverWait(self.browser.driver, 20).until(
|
583
|
+
EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.ace_text-input"))
|
584
|
+
)
|
585
|
+
self.browser.driver.execute_script("arguments[0].focus();", text_input)
|
586
|
+
WebDriverWait(self.browser.driver, 1).until(
|
587
|
+
EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.ace_text-input:focus"))
|
588
|
+
)
|
589
|
+
text_input.send_keys(Keys.CONTROL + 'a')
|
590
|
+
text_input.send_keys(Keys.DELETE)
|
591
|
+
self.browser.driver.execute_script(
|
592
|
+
"ace.edit(document.querySelector('.ace_editor')).setValue(arguments[0], -1);",
|
593
|
+
code_text
|
594
|
+
)
|
595
|
+
return True
|
596
|
+
except Exception as e:
|
597
|
+
print(f"{Fore.RED} Error loading code: {e}")
|
598
|
+
return False
|
599
|
+
|
600
|
+
def verdict(self):
|
601
|
+
# Whether your solution was CORRECT or NOT ??
|
602
|
+
sol_verd = self.browser.driver.find_element(By.XPATH, "//div[contains(@class, '_run__container')]").text
|
603
|
+
sol_verd = sol_verd[0]
|
604
|
+
return sol_verd.lower() == 'c'
|
605
|
+
|
606
|
+
def submit_solution(self, problem_num: int = None):
|
607
|
+
# Submit solution for a problem
|
608
|
+
|
609
|
+
curr_url = self.browser.driver.current_url
|
610
|
+
if not self.login():
|
611
|
+
self.login()
|
612
|
+
|
613
|
+
self.browser.driver.get(curr_url)
|
614
|
+
|
615
|
+
if not self.problems or not self.tabs:
|
616
|
+
print(f"{Fore.RED} No problems loaded. Please open contest tab first.")
|
617
|
+
return False
|
618
|
+
|
619
|
+
if problem_num is None:
|
620
|
+
problem_num = click.prompt(f"{Fore.MAGENTA} Enter problem number to submit", type=int, default=1)
|
621
|
+
|
622
|
+
if problem_num < 1 or problem_num > len(self.problems):
|
623
|
+
print(f"{Fore.RED} Invalid problem number. Please enter a valid number (1-{len(self.problems)}).")
|
624
|
+
return False
|
625
|
+
|
626
|
+
# Switch to selected problem tab
|
627
|
+
self.browser.driver.switch_to.window(self.tabs[problem_num])
|
628
|
+
|
629
|
+
if not self._select_language():
|
630
|
+
return False
|
631
|
+
|
632
|
+
# Try to find solution file created by solve() function
|
633
|
+
solution_path = self._find_solution_file(problem_num)
|
634
|
+
|
635
|
+
# If solution file not found, prompt for path
|
636
|
+
if not solution_path:
|
637
|
+
solution_path = self._get_solution_path()
|
638
|
+
|
639
|
+
if not solution_path:
|
640
|
+
return False
|
641
|
+
|
642
|
+
try:
|
643
|
+
with open(solution_path, 'r') as f:
|
644
|
+
code_text = f.read()
|
645
|
+
print(f"{Fore.GREEN} Using solution file: {solution_path}")
|
646
|
+
except Exception as e:
|
647
|
+
print(f"{Fore.RED} Error reading solution file: {e}")
|
648
|
+
return False
|
649
|
+
|
650
|
+
if not self._load_code(code_text):
|
651
|
+
return False
|
652
|
+
|
653
|
+
# Submit the Code
|
654
|
+
try:
|
655
|
+
text_input = self.browser.driver.find_element(By.CSS_SELECTOR, "textarea.ace_text-input")
|
656
|
+
text_input.send_keys(Keys.CONTROL + Keys.ENTER)
|
657
|
+
self.type_writer(" Submitting the Code ... ")
|
658
|
+
|
659
|
+
WebDriverWait(self.browser.driver, 20).until(
|
660
|
+
EC.presence_of_element_located((By.XPATH, "//div[contains(@class, '_run__container')]"))
|
661
|
+
)
|
662
|
+
|
663
|
+
verdict_in_run_container = self.browser.driver.find_element(By.XPATH, "//div[contains(@class, '_run__container')]").text
|
664
|
+
if 'Error' in verdict_in_run_container or 'limit' in verdict_in_run_container.lower():
|
665
|
+
print(f"{Fore.RED} {verdict_in_run_container}\n")
|
666
|
+
return False
|
667
|
+
|
668
|
+
# Wait for the verdict table to appear
|
669
|
+
WebDriverWait(self.browser.driver, 20).until(
|
670
|
+
EC.presence_of_element_located((By.XPATH, "//table[contains(@class, 'status-table')]"))
|
671
|
+
)
|
672
|
+
|
673
|
+
# Extract table data
|
674
|
+
table_element = self.browser.driver.find_element(By.XPATH, "//table[contains(@class, 'status-table')]")
|
675
|
+
rows = table_element.find_elements(By.TAG_NAME, "tr")
|
676
|
+
|
677
|
+
# Parse the table data
|
678
|
+
table_data = []
|
679
|
+
for row in rows:
|
680
|
+
cells = row.find_elements(By.TAG_NAME, "td")
|
681
|
+
if cells:
|
682
|
+
row_data = [cell.text for cell in cells]
|
683
|
+
table_data.append(row_data)
|
684
|
+
|
685
|
+
final_verdict = table_data.pop()
|
686
|
+
|
687
|
+
final_table_data = []
|
688
|
+
for data in table_data:
|
689
|
+
interm = []
|
690
|
+
if 'Correct' in data[-1]:
|
691
|
+
for i in range(0, 3):
|
692
|
+
if i != 2: interm.append(f"{Fore.GREEN}{data[i]}{Style.RESET_ALL}")
|
693
|
+
else:
|
694
|
+
string = ''
|
695
|
+
for char in data[i]:
|
696
|
+
if char != '\n': string += f'{Fore.GREEN}{char}{Style.RESET_ALL}'
|
697
|
+
else: string += f'{char}'
|
698
|
+
interm.append(string)
|
699
|
+
elif 'Wrong Answer' in data[-1]:
|
700
|
+
for i in range(0, 3):
|
701
|
+
if i != 2: interm.append(f"{Fore.RED}{data[i]}{Style.RESET_ALL}")
|
702
|
+
else:
|
703
|
+
string = ''
|
704
|
+
for char in data[i]:
|
705
|
+
if char != '\n': string += f'{Fore.RED}{char}{Style.RESET_ALL}'
|
706
|
+
else: string += f'{char}'
|
707
|
+
interm.append(string)
|
708
|
+
else:
|
709
|
+
for i in data:
|
710
|
+
interm.append(f"{i}")
|
711
|
+
final_table_data.append(interm)
|
712
|
+
|
713
|
+
# Create a pandas DataFrame
|
714
|
+
if 'Correct' in final_verdict[1]:
|
715
|
+
df = pd.DataFrame(final_table_data, columns=[f"{Fore.GREEN}Sub-Task{Style.RESET_ALL}",
|
716
|
+
f"{Fore.GREEN}Task #{Style.RESET_ALL}",
|
717
|
+
f"{Fore.GREEN}Result (time){Style.RESET_ALL}"])
|
718
|
+
else:
|
719
|
+
df = pd.DataFrame(final_table_data, columns=[f"{Fore.RED}Sub-Task{Style.RESET_ALL}",
|
720
|
+
f"{Fore.RED}Task #{Style.RESET_ALL}",
|
721
|
+
f"{Fore.RED}Result (time){Style.RESET_ALL}"])
|
722
|
+
|
723
|
+
print(f"\n{Fore.CYAN} Verdict Table:")
|
724
|
+
print(tabulate(
|
725
|
+
df,
|
726
|
+
headers='keys',
|
727
|
+
tablefmt='fancy_grid',
|
728
|
+
showindex=False,
|
729
|
+
stralign='center',
|
730
|
+
numalign='center',
|
731
|
+
colalign=('center', 'center', 'center')
|
732
|
+
))
|
733
|
+
print(Style.RESET_ALL)
|
734
|
+
|
735
|
+
print(f" {Fore.YELLOW}{final_verdict[0]}")
|
736
|
+
|
737
|
+
code = self.problems[problem_num - 1][0]
|
738
|
+
|
739
|
+
if "Correct" in final_verdict[1]:
|
740
|
+
print(f"{Fore.GREEN} VERDICT: CORRECT {Fore.GREEN}\u2714\n{Style.RESET_ALL}")
|
741
|
+
self.problem_status[code] = "done"
|
742
|
+
|
743
|
+
elif "Wrong" in final_verdict[1]:
|
744
|
+
print(f"{Fore.RED} VERDICT: INCORRECT {Fore.RED}\u2716\n{Style.RESET_ALL}")
|
745
|
+
if self.problem_status.get(code) != "done": # Optional: don't overwrite if already done
|
746
|
+
self.problem_status[code] = "wrong"
|
747
|
+
|
748
|
+
else:
|
749
|
+
self.problem_status[code] = "pending"
|
750
|
+
|
751
|
+
return True
|
752
|
+
except Exception as e:
|
753
|
+
print(f"{Fore.RED} Error submitting code: {e}")
|
754
|
+
traceback.print_exc() # Print the full traceback for better debugging
|
755
|
+
return False
|
756
|
+
# -------------------------------------------------------------------------------------
|
757
|
+
|
758
|
+
# ------------------ Checker Functions ----------------------
|
759
|
+
def demo_cases_check(self, problem_num: int = None):
|
760
|
+
"""
|
761
|
+
Runs all sample test cases for a problem, displays the outputs in a table,
|
762
|
+
then compares the outputs line-by-line (ignoring extra whitespace and case)
|
763
|
+
and prints a detailed verdict message inspired by verdict.py.
|
764
|
+
"""
|
765
|
+
import re # For whitespace normalization
|
766
|
+
|
767
|
+
# ANSI color codes (from verdict.py)
|
768
|
+
GREEN_TICK = '\u2705'
|
769
|
+
RED_CR0SS = '\u274c'
|
770
|
+
BOLD_YELLOW = '\033[1;33m'
|
771
|
+
GREEN = '\033[0;32m'
|
772
|
+
RED = '\x1b[91m'
|
773
|
+
RESET = '\033[0m'
|
774
|
+
|
775
|
+
def normalize_whitespace(text):
|
776
|
+
return re.sub(r'\s+', ' ', text.strip())
|
777
|
+
|
778
|
+
if not self.problems or not self.tabs:
|
779
|
+
print(f"{Fore.RED} No problems loaded. Please open a contest page first.")
|
780
|
+
return False
|
781
|
+
|
782
|
+
if problem_num is None:
|
783
|
+
problem_num = click.prompt(f"{Fore.MAGENTA} Enter problem number to check", type=int, default=1)
|
784
|
+
|
785
|
+
if problem_num < 1 or problem_num > len(self.problems):
|
786
|
+
print(f"{Fore.RED} Invalid problem number. Please enter a valid number (1-{len(self.problems)}).")
|
787
|
+
return False
|
788
|
+
|
789
|
+
# Switch to the appropriate problem tab and select language
|
790
|
+
self.browser.driver.switch_to.window(self.tabs[problem_num])
|
791
|
+
if not self._select_language():
|
792
|
+
return False
|
793
|
+
|
794
|
+
# Locate solution file (from solve() or prompt)
|
795
|
+
solution_path = self._find_solution_file(problem_num)
|
796
|
+
if not solution_path:
|
797
|
+
solution_path = self._get_solution_path()
|
798
|
+
if not solution_path:
|
799
|
+
return False
|
800
|
+
|
801
|
+
try:
|
802
|
+
with open(solution_path, 'r') as f:
|
803
|
+
code_text = f.read()
|
804
|
+
print(f"{Fore.GREEN} Using solution file: {solution_path}")
|
805
|
+
except Exception as e:
|
806
|
+
print(f"{Fore.RED} Error reading solution file: {e}")
|
807
|
+
return False
|
808
|
+
if not self._load_code(code_text):
|
809
|
+
return False
|
810
|
+
|
811
|
+
self.type_writer(" Checking Demo test cases ...")
|
812
|
+
|
813
|
+
# Run test cases by sending keystrokes
|
814
|
+
try:
|
815
|
+
text_input = self.browser.driver.find_element(By.CSS_SELECTOR, "textarea.ace_text-input")
|
816
|
+
text_input.send_keys(Keys.CONTROL + Keys.SHIFT + Keys.ENTER)
|
817
|
+
WebDriverWait(self.browser.driver, 20).until(
|
818
|
+
EC.presence_of_element_located((By.XPATH, "//div[contains(@class, '_output-item__value_')]"))
|
819
|
+
)
|
820
|
+
except Exception as e:
|
821
|
+
print(f"{Fore.RED} Error running test cases: {e}")
|
822
|
+
return False
|
823
|
+
|
824
|
+
# Retrieve outputs
|
825
|
+
try:
|
826
|
+
actual_elem = self.browser.driver.find_elements(By.XPATH, "//div[contains(@class, '_output-item__value_')]")
|
827
|
+
actual_output_text = actual_elem[-1].text
|
828
|
+
expected_elem = self.browser.driver.find_elements(By.XPATH, "//div[contains(@class, '_values_')]")
|
829
|
+
expected_output_text = expected_elem[-1].text
|
830
|
+
except Exception as e:
|
831
|
+
print(f"{Fore.RED} Error retrieving outputs: {e}")
|
832
|
+
return False
|
833
|
+
|
834
|
+
status_bar = self.browser.driver.find_element(By.XPATH, "//div[contains(@class, '_status__container_')]").text
|
835
|
+
status_text = str(status_bar.split(":")[-1].strip())
|
836
|
+
if 'error' in status_text or 'limit' in status_text:
|
837
|
+
print(f"{Fore.RED} {status_text} {RESET}\n")
|
838
|
+
return False
|
839
|
+
|
840
|
+
full_text = expected_elem[0].text.strip()
|
841
|
+
expected_output_text = expected_elem[-1].text.strip()
|
842
|
+
|
843
|
+
if expected_output_text and expected_output_text in full_text:
|
844
|
+
input_text = full_text.replace(expected_output_text, '').strip()
|
845
|
+
else:
|
846
|
+
input_text = full_text.strip()
|
847
|
+
|
848
|
+
if actual_output_text.strip() == input_text:
|
849
|
+
print(f"{Fore.YELLOW} NO OUTPUT{RESET}\n")
|
850
|
+
return False
|
851
|
+
|
852
|
+
# Pre-check: Time Limit Exceeded and no-output conditions
|
853
|
+
normalized_actual_full = normalize_whitespace(actual_output_text)
|
854
|
+
normalized_expected_full = normalize_whitespace(expected_output_text)
|
855
|
+
if "time limit exceeded" in normalized_actual_full.lower():
|
856
|
+
print(f"{BOLD_YELLOW} VERDICT:{RESET} {RED}TIME LIMIT EXCEEDED {RED_CR0SS}{RESET}\n")
|
857
|
+
return True
|
858
|
+
if not normalized_actual_full and normalized_expected_full:
|
859
|
+
print(f"{BOLD_YELLOW} VERDICT:{RESET} {RED}TIME LIMIT EXCEEDED ⏰{RESET}\n")
|
860
|
+
return True
|
861
|
+
|
862
|
+
# Split outputs into lines and normalize each line for comparison
|
863
|
+
actual_lines = [normalize_whitespace(line) for line in actual_output_text.splitlines()]
|
864
|
+
expected_lines = [normalize_whitespace(line) for line in expected_output_text.splitlines()]
|
865
|
+
|
866
|
+
# Ensure both lists have the same length by padding with empty strings
|
867
|
+
max_len = max(len(actual_lines), len(expected_lines))
|
868
|
+
actual_lines.extend([''] * (max_len - len(actual_lines)))
|
869
|
+
expected_lines.extend([''] * (max_len - len(expected_lines)))
|
870
|
+
|
871
|
+
# Build a table to display outputs
|
872
|
+
headers = ['Your Output', 'Expected Output']
|
873
|
+
table_rows = []
|
874
|
+
for u, e in zip(actual_lines, expected_lines):
|
875
|
+
# Color entire rows in green if they match, otherwise highlight differences
|
876
|
+
if u.lower() == e.lower():
|
877
|
+
table_rows.append([f"{GREEN}{u}{RESET}", f"{GREEN}{e}{RESET}"])
|
878
|
+
else:
|
879
|
+
table_rows.append([f"{RED}{u}{RESET}", f"{GREEN}{e}{RESET}"])
|
880
|
+
print()
|
881
|
+
print(tabulate(
|
882
|
+
table_rows,
|
883
|
+
headers=headers,
|
884
|
+
tablefmt='fancy_grid',
|
885
|
+
showindex=False,
|
886
|
+
stralign='center',
|
887
|
+
numalign='center',
|
888
|
+
colalign=('center', 'center')
|
889
|
+
))
|
890
|
+
print()
|
891
|
+
|
892
|
+
# Line-by-line comparison to detect the first mismatch
|
893
|
+
mismatch_found = False
|
894
|
+
for i, (exp, act) in enumerate(zip(expected_lines, actual_lines), start=1):
|
895
|
+
if exp.lower() != act.lower():
|
896
|
+
print(f"{BOLD_YELLOW} VERDICT:{RESET} {RED}WRONG ANSWER {RED_CR0SS}{RESET}\n")
|
897
|
+
print(f"{RED} INCORRECT AT LINE: {i}")
|
898
|
+
print(f"{GREEN} EXPECTED: {GREEN}{exp}{RESET}")
|
899
|
+
print(f"{BOLD_YELLOW} FOUND: {RED}{act}{RESET}\n")
|
900
|
+
mismatch_found = True
|
901
|
+
break
|
902
|
+
if not mismatch_found and len(actual_lines) == len(expected_lines):
|
903
|
+
print(f"{BOLD_YELLOW} VERDICT:{RESET} {GREEN}CORRECT {GREEN_TICK}{RESET}\n")
|
904
|
+
elif not mismatch_found:
|
905
|
+
# If line counts differ, treat as mismatch
|
906
|
+
print(f"{BOLD_YELLOW} VERDICT:{RESET} {RED}WRONG ANSWER {RED_CR0SS}{RESET}\n")
|
907
|
+
if len(expected_lines) > len(actual_lines):
|
908
|
+
print(f"{RED} INCORRECT AT LINE: {len(actual_lines) + 1}")
|
909
|
+
print(f"{GREEN} EXPECTED: {GREEN}{expected_lines[len(actual_lines)]}{RESET}")
|
910
|
+
print(f"{BOLD_YELLOW} FOUND: {RED}NULL{RESET}\n")
|
911
|
+
else:
|
912
|
+
print(f"{RED} INCORRECT AT LINE: {len(expected_lines) + 1}")
|
913
|
+
print(f"{GREEN} EXPECTED: {GREEN}NULL{RESET}")
|
914
|
+
print(f"{BOLD_YELLOW} FOUND: {RED}{actual_lines[len(expected_lines)]}{RESET}\n")
|
915
|
+
return True
|
916
|
+
|
917
|
+
def solve(self):
|
918
|
+
"""
|
919
|
+
Create solution files for problems and open selected problem for editing
|
920
|
+
"""
|
921
|
+
if not self.problems or not self.tabs:
|
922
|
+
print(f"{Fore.RED} No problems loaded. Please open a contest first.")
|
923
|
+
return False
|
924
|
+
|
925
|
+
# Get solution directory from config
|
926
|
+
solution_path = self.config.get("solution_path", "")
|
927
|
+
if not solution_path:
|
928
|
+
solution_path = click.prompt(" Enter solution directory path")
|
929
|
+
save_path = click.confirm(" Save this path for future use?", default=True)
|
930
|
+
if save_path:
|
931
|
+
self.config.set("solution_path", solution_path)
|
932
|
+
|
933
|
+
self.type_writer(" Creating Directories ... ")
|
934
|
+
|
935
|
+
# Create solution directory if it doesn't exist
|
936
|
+
if not os.path.exists(solution_path):
|
937
|
+
try:
|
938
|
+
os.makedirs(solution_path)
|
939
|
+
print(f"{Fore.GREEN} Created solution directory: {solution_path}")
|
940
|
+
except Exception as e:
|
941
|
+
print(f"{Fore.RED} Error creating solution directory: {e}")
|
942
|
+
return False
|
943
|
+
|
944
|
+
# Get preferred language
|
945
|
+
language = self.config.get("preferred_language", "")
|
946
|
+
if not language:
|
947
|
+
language = click.prompt(" Enter your preferred language (e.g., C++)")
|
948
|
+
save_lang = click.confirm(" Save this language preference?", default=True)
|
949
|
+
if save_lang:
|
950
|
+
self.config.set("preferred_language", language)
|
951
|
+
|
952
|
+
# Get file extension for the language
|
953
|
+
extension = LANGUAGE_EXTENSIONS.get(language, ".txt")
|
954
|
+
if extension == ".txt":
|
955
|
+
print(f"{Fore.YELLOW} Warning: Unknown language '{language}'. Using .txt extension.")
|
956
|
+
|
957
|
+
# Use existing contest ID or extract contest ID from URL
|
958
|
+
if self.contest_id is None and self.tabs and self.browser.driver:
|
959
|
+
current_url = self.browser.driver.current_url
|
960
|
+
if "START" in current_url:
|
961
|
+
try:
|
962
|
+
# Extract the contest number from START{NUM}{DIVISION}
|
963
|
+
# For example: https://www.codechef.com/START171A -> 171
|
964
|
+
self.contest_id = ''.join(filter(str.isdigit, current_url.split("START")[1][:4]))
|
965
|
+
print(f"{Fore.GREEN} Detected contest ID: {self.contest_id}")
|
966
|
+
except:
|
967
|
+
self.contest_id = "unknown"
|
968
|
+
else:
|
969
|
+
self.contest_id = "unknown"
|
970
|
+
|
971
|
+
# Use existing contest directory or create a new one
|
972
|
+
if self.contest_dir and os.path.exists(self.contest_dir):
|
973
|
+
contest_dir = self.contest_dir
|
974
|
+
print(f"{Fore.GREEN} Using existing contest directory: {contest_dir}")
|
975
|
+
else:
|
976
|
+
# Create Starter directory with contest number if it doesn't exist
|
977
|
+
contest_dir = os.path.join(solution_path, f"Starter {self.contest_id}")
|
978
|
+
|
979
|
+
# Check if the directory already exists
|
980
|
+
if os.path.exists(contest_dir):
|
981
|
+
print(f"{Fore.GREEN} Using existing contest directory: {contest_dir}")
|
982
|
+
else:
|
983
|
+
try:
|
984
|
+
os.makedirs(contest_dir)
|
985
|
+
print(f"{Fore.GREEN} Created contest directory: {contest_dir}")
|
986
|
+
except Exception as e:
|
987
|
+
print(f"{Fore.RED} Error creating contest directory: {e}")
|
988
|
+
return False
|
989
|
+
|
990
|
+
# Store the contest directory for future use
|
991
|
+
self.contest_dir = contest_dir
|
992
|
+
|
993
|
+
# Save contest directory in config for other functions to use
|
994
|
+
self.config.set("current_contest_dir", contest_dir)
|
995
|
+
|
996
|
+
# Get username for author comment
|
997
|
+
ur_name = self.config.get("username", "USER")
|
998
|
+
|
999
|
+
# Create template file content based on language
|
1000
|
+
template_content = ""
|
1001
|
+
if language == "C++":
|
1002
|
+
template_content = f"""// Author -> '{ur_name}'
|
1003
|
+
|
1004
|
+
#include <bits/stdc++.h>
|
1005
|
+
using namespace std;
|
1006
|
+
|
1007
|
+
int main() {{
|
1008
|
+
ios_base::sync_with_stdio(false);
|
1009
|
+
cin.tie(NULL);
|
1010
|
+
|
1011
|
+
// Your code here
|
1012
|
+
|
1013
|
+
return 0;
|
1014
|
+
}}
|
1015
|
+
"""
|
1016
|
+
elif language == "Python3" or language == "Python 3" or language == "PyPy3":
|
1017
|
+
template_content = f"""# Author -> '{ur_name}'
|
1018
|
+
|
1019
|
+
def main():
|
1020
|
+
# Your code here
|
1021
|
+
pass
|
1022
|
+
|
1023
|
+
if __name__ == "__main__":
|
1024
|
+
main()
|
1025
|
+
"""
|
1026
|
+
elif language == "Java":
|
1027
|
+
template_content = f"""// Author -> '{ur_name}'
|
1028
|
+
import java.util.*;
|
1029
|
+
|
1030
|
+
public class Main {{
|
1031
|
+
public static void main(String[] args) {{
|
1032
|
+
Scanner sc = new Scanner(System.in);
|
1033
|
+
|
1034
|
+
// Your code here
|
1035
|
+
|
1036
|
+
sc.close();
|
1037
|
+
}}
|
1038
|
+
}}
|
1039
|
+
"""
|
1040
|
+
elif language == "JavaScript" or language == "NodeJS" or language == "NODEJS":
|
1041
|
+
template_content = f"""// Author -> '{ur_name}'
|
1042
|
+
|
1043
|
+
function main() {{
|
1044
|
+
// Your code here
|
1045
|
+
}}
|
1046
|
+
|
1047
|
+
main();
|
1048
|
+
"""
|
1049
|
+
elif language == "Go":
|
1050
|
+
template_content = f"""// Author -> '{ur_name}'
|
1051
|
+
package main
|
1052
|
+
|
1053
|
+
import (
|
1054
|
+
"fmt"
|
1055
|
+
)
|
1056
|
+
|
1057
|
+
func main() {{
|
1058
|
+
// Your code here
|
1059
|
+
}}
|
1060
|
+
"""
|
1061
|
+
elif language == "C#" or language == "CS":
|
1062
|
+
template_content = f"""// Author -> '{ur_name}'
|
1063
|
+
using System;
|
1064
|
+
|
1065
|
+
class Program {{
|
1066
|
+
static void Main(string[] args) {{
|
1067
|
+
// Your code here
|
1068
|
+
}}
|
1069
|
+
}}
|
1070
|
+
"""
|
1071
|
+
elif language == "PHP":
|
1072
|
+
template_content = f"""<?php
|
1073
|
+
// Author -> '{ur_name}'
|
1074
|
+
|
1075
|
+
function main() {{
|
1076
|
+
// Your code here
|
1077
|
+
}}
|
1078
|
+
|
1079
|
+
main();
|
1080
|
+
?>
|
1081
|
+
"""
|
1082
|
+
elif language == "Ruby":
|
1083
|
+
template_content = f"""# Author -> '{ur_name}'
|
1084
|
+
|
1085
|
+
def main
|
1086
|
+
# Your code here
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
main
|
1090
|
+
"""
|
1091
|
+
elif language == "Rust":
|
1092
|
+
template_content = f"""// Author -> '{ur_name}'
|
1093
|
+
|
1094
|
+
fn main() {{
|
1095
|
+
// Your code here
|
1096
|
+
}}
|
1097
|
+
"""
|
1098
|
+
elif language == "TypeScript" or language == "TS":
|
1099
|
+
template_content = f"""// Author -> '{ur_name}'
|
1100
|
+
|
1101
|
+
function main(): void {{
|
1102
|
+
// Your code here
|
1103
|
+
}}
|
1104
|
+
|
1105
|
+
main();
|
1106
|
+
"""
|
1107
|
+
elif language == "Kotlin" or language == "KTLN":
|
1108
|
+
template_content = f"""// Author -> '{ur_name}'
|
1109
|
+
|
1110
|
+
fun main() {{
|
1111
|
+
// Your code here
|
1112
|
+
}}
|
1113
|
+
"""
|
1114
|
+
elif language == "R":
|
1115
|
+
template_content = f"""# Author -> '{ur_name}'
|
1116
|
+
|
1117
|
+
main <- function() {{
|
1118
|
+
# Your code here
|
1119
|
+
}}
|
1120
|
+
|
1121
|
+
main()
|
1122
|
+
"""
|
1123
|
+
elif language == "C":
|
1124
|
+
template_content = f"""// Author -> '{ur_name}'
|
1125
|
+
|
1126
|
+
#include <stdio.h>
|
1127
|
+
|
1128
|
+
int main() {{
|
1129
|
+
// Your code here
|
1130
|
+
return 0;
|
1131
|
+
}}
|
1132
|
+
"""
|
1133
|
+
else:
|
1134
|
+
template_content = f"""// Author -> '{ur_name}'
|
1135
|
+
|
1136
|
+
// Your code here
|
1137
|
+
"""
|
1138
|
+
|
1139
|
+
# Create solution files for each problem (only if they don't exist)
|
1140
|
+
created_files = []
|
1141
|
+
existing_files = []
|
1142
|
+
|
1143
|
+
for idx, (code, _, _) in enumerate(self.problems, start=1):
|
1144
|
+
filename = f"{code}{extension}"
|
1145
|
+
filepath = os.path.join(contest_dir, filename)
|
1146
|
+
|
1147
|
+
# Check if file already exists
|
1148
|
+
if os.path.exists(filepath):
|
1149
|
+
existing_files.append((idx, filepath))
|
1150
|
+
else:
|
1151
|
+
try:
|
1152
|
+
with open(filepath, 'w') as f:
|
1153
|
+
f.write(template_content)
|
1154
|
+
print(f"{Fore.GREEN} Created solution file: {filepath}")
|
1155
|
+
created_files.append((idx, filepath))
|
1156
|
+
except Exception as e:
|
1157
|
+
print(f"{Fore.RED} Error creating solution file: {filepath}: {e}")
|
1158
|
+
|
1159
|
+
# Combine created and existing files (sorted by index)
|
1160
|
+
all_files = sorted(created_files + existing_files, key=lambda x: x[0])
|
1161
|
+
|
1162
|
+
# Display files
|
1163
|
+
if created_files:
|
1164
|
+
print(f"\n{Fore.GREEN} New solution files created for {len(created_files)} problems:")
|
1165
|
+
for idx, filepath in created_files:
|
1166
|
+
print(f" {idx}. {os.path.basename(filepath)}")
|
1167
|
+
|
1168
|
+
if existing_files:
|
1169
|
+
print(f"\n{Fore.CYAN} Using existing solution files for {len(existing_files)} problems:")
|
1170
|
+
for idx, filepath in existing_files:
|
1171
|
+
print(f" {idx}. {os.path.basename(filepath)}")
|
1172
|
+
|
1173
|
+
# Select problem to solve
|
1174
|
+
print(f"\n{Fore.CYAN} Available problems:")
|
1175
|
+
for idx, (code, title, _) in enumerate(self.problems, start=1):
|
1176
|
+
print(f" {idx}. {code}: {title}")
|
1177
|
+
|
1178
|
+
print()
|
1179
|
+
problem_num = click.prompt(f"{Fore.MAGENTA} Enter problem number to solve", type=int, default=1)
|
1180
|
+
print()
|
1181
|
+
if problem_num < 1 or problem_num > len(self.problems):
|
1182
|
+
print(f"{Fore.RED} Invalid problem number. Please enter a valid number (1-{len(self.problems)}).")
|
1183
|
+
return False
|
1184
|
+
|
1185
|
+
# Find the file for the selected problem
|
1186
|
+
selected_file = None
|
1187
|
+
for idx, filepath in all_files:
|
1188
|
+
if idx == problem_num:
|
1189
|
+
selected_file = filepath
|
1190
|
+
break
|
1191
|
+
|
1192
|
+
if not selected_file:
|
1193
|
+
print(f"{Fore.RED} No solution file found for problem {problem_num}.")
|
1194
|
+
return False
|
1195
|
+
|
1196
|
+
# Open selected problem file
|
1197
|
+
try:
|
1198
|
+
# Switch to problem tab first
|
1199
|
+
self.browser.driver.switch_to.window(self.tabs[problem_num])
|
1200
|
+
print(f"{Fore.GREEN} Switched to problem tab: {self.problems[problem_num-1][1]}")
|
1201
|
+
|
1202
|
+
# Check if file exists
|
1203
|
+
if not os.path.exists(selected_file):
|
1204
|
+
print(f"{Fore.RED} File not found: {selected_file}")
|
1205
|
+
return False
|
1206
|
+
|
1207
|
+
# Get preferred editor from config or prompt
|
1208
|
+
editor = self.config.get("preferred_editor", "")
|
1209
|
+
if not editor:
|
1210
|
+
choice = 1
|
1211
|
+
editor = [
|
1212
|
+
Choice(value="default", name="Notepad (System Default)"),
|
1213
|
+
Choice(value="vscode", name="VS Code"),
|
1214
|
+
Choice(value="sublime", name="Sublime Text"),
|
1215
|
+
Choice(value="notepad++", name="Notepad++ (Windows)"),
|
1216
|
+
Choice(value="atom", name="Atom"),
|
1217
|
+
]
|
1218
|
+
|
1219
|
+
editor_selector = inquirer.select(
|
1220
|
+
message="Select your preferred editor:",
|
1221
|
+
choices=editor,
|
1222
|
+
default="default", # Use the stored current choice
|
1223
|
+
qmark="", # Remove the default [?]
|
1224
|
+
pointer=">", # Custom arrow pointer
|
1225
|
+
instruction="(Use arrow keys to navigate)"
|
1226
|
+
).execute()
|
1227
|
+
|
1228
|
+
choice = editor_selector
|
1229
|
+
selected_editor = next((c.name for c in editor if c.value == editor_selector), editor_selector)
|
1230
|
+
print(f"{Fore.GREEN} You chose: {selected_editor}")
|
1231
|
+
|
1232
|
+
if choice == 1:
|
1233
|
+
editor = "default"
|
1234
|
+
elif choice == 2:
|
1235
|
+
editor = "vscode"
|
1236
|
+
elif choice == 3:
|
1237
|
+
editor = "sublime"
|
1238
|
+
elif choice == 4:
|
1239
|
+
editor = "notepad++"
|
1240
|
+
elif choice == 5:
|
1241
|
+
editor = "atom"
|
1242
|
+
elif choice == 6:
|
1243
|
+
editor = click.prompt(" Enter editor command (use {file} as placeholder for filename)")
|
1244
|
+
|
1245
|
+
save_choice = click.confirm(" Save this editor preference?", default=True)
|
1246
|
+
if save_choice:
|
1247
|
+
self.config.set("preferred_editor", editor)
|
1248
|
+
|
1249
|
+
# Open file with selected editor
|
1250
|
+
print(f"{Fore.GREEN} Opening file with editor: {editor}")
|
1251
|
+
|
1252
|
+
devnull = subprocess.DEVNULL
|
1253
|
+
|
1254
|
+
try:
|
1255
|
+
if editor == "default":
|
1256
|
+
# Use system default
|
1257
|
+
if os.name == 'nt': # Windows
|
1258
|
+
subprocess.Popen(['notepad', selected_file],
|
1259
|
+
start_new_session=True,
|
1260
|
+
shell=True,
|
1261
|
+
stdout=devnull,
|
1262
|
+
stderr=devnull)
|
1263
|
+
time.sleep(0.3)
|
1264
|
+
self.flush_stdin()
|
1265
|
+
elif os.name == 'posix': # Linux/Mac
|
1266
|
+
if sys.platform == 'darwin': # Mac
|
1267
|
+
subprocess.call(['open', selected_file], stdout=devnull, stderr=devnull)
|
1268
|
+
time.sleep(0.3)
|
1269
|
+
self.flush_stdin()
|
1270
|
+
else: # Linux
|
1271
|
+
subprocess.call(['xdg-open', selected_file], stdout=devnull, stderr=devnull)
|
1272
|
+
time.sleep(0.3)
|
1273
|
+
self.flush_stdin()
|
1274
|
+
elif editor == "vscode":
|
1275
|
+
# Check for different VS Code executable names
|
1276
|
+
vscode_commands = ['code', 'Code.exe', 'code.cmd', 'Code.cmd']
|
1277
|
+
success = False
|
1278
|
+
for cmd in vscode_commands:
|
1279
|
+
try:
|
1280
|
+
subprocess.Popen([cmd, selected_file],
|
1281
|
+
start_new_session=True,
|
1282
|
+
shell=(os.name == 'nt'),
|
1283
|
+
stdout=devnull,
|
1284
|
+
stderr=devnull)
|
1285
|
+
time.sleep(0.3)
|
1286
|
+
self.flush_stdin()
|
1287
|
+
success = True
|
1288
|
+
break
|
1289
|
+
except FileNotFoundError:
|
1290
|
+
continue
|
1291
|
+
|
1292
|
+
if not success:
|
1293
|
+
print(f"{Fore.YELLOW} VS Code not found in PATH. Falling back to system default.")
|
1294
|
+
print(f"{Fore.YELLOW} TIP - Add the path of vscode.exe file into the system's environment variable path.")
|
1295
|
+
# Fall back to system default
|
1296
|
+
if os.name == 'nt':
|
1297
|
+
subprocess.Popen(['notepad', selected_file],
|
1298
|
+
start_new_session=True,
|
1299
|
+
shell=True,
|
1300
|
+
stdout=devnull,
|
1301
|
+
stderr=devnull)
|
1302
|
+
time.sleep(0.3)
|
1303
|
+
self.flush_stdin()
|
1304
|
+
elif os.name == 'posix':
|
1305
|
+
if sys.platform == 'darwin':
|
1306
|
+
subprocess.call(['open', selected_file], stdout=devnull, stderr=devnull)
|
1307
|
+
time.sleep(0.3)
|
1308
|
+
self.flush_stdin()
|
1309
|
+
else:
|
1310
|
+
subprocess.call(['xdg-open', selected_file], stdout=devnull, stderr=devnull)
|
1311
|
+
time.sleep(0.3)
|
1312
|
+
self.flush_stdin()
|
1313
|
+
elif editor == "sublime":
|
1314
|
+
# Try different possible Sublime Text command names
|
1315
|
+
sublime_commands = ['subl', 'sublime_text', 'sublime', 'Sublime Text.exe']
|
1316
|
+
success = False
|
1317
|
+
for cmd in sublime_commands:
|
1318
|
+
try:
|
1319
|
+
subprocess.Popen([cmd, selected_file],
|
1320
|
+
start_new_session=True,
|
1321
|
+
shell=(os.name == 'nt'),
|
1322
|
+
stdout=devnull,
|
1323
|
+
stderr=devnull)
|
1324
|
+
time.sleep(0.3)
|
1325
|
+
self.flush_stdin()
|
1326
|
+
success = True
|
1327
|
+
break
|
1328
|
+
except FileNotFoundError:
|
1329
|
+
continue
|
1330
|
+
|
1331
|
+
if not success:
|
1332
|
+
print(f"{Fore.YELLOW} Sublime Text not found in PATH. Falling back to system default.")
|
1333
|
+
print(f"{Fore.YELLOW} TIP - Add the path of sublime_text.exe file into the system's environment variable path.")
|
1334
|
+
# Fall back to system default
|
1335
|
+
if os.name == 'nt':
|
1336
|
+
subprocess.Popen(['notepad', selected_file],
|
1337
|
+
start_new_session=True,
|
1338
|
+
shell=True,
|
1339
|
+
stdout=devnull,
|
1340
|
+
stderr=devnull)
|
1341
|
+
time.sleep(0.3)
|
1342
|
+
self.flush_stdin()
|
1343
|
+
else:
|
1344
|
+
subprocess.call(['xdg-open', selected_file], stdout=devnull, stderr=devnull)
|
1345
|
+
time.sleep(0.3)
|
1346
|
+
self.flush_stdin()
|
1347
|
+
elif editor == "notepad++":
|
1348
|
+
try:
|
1349
|
+
# Try different possible Notepad++ executable names
|
1350
|
+
npp_commands = ['notepad++', 'notepad++.exe', 'Notepad++.exe']
|
1351
|
+
success = False
|
1352
|
+
for cmd in npp_commands:
|
1353
|
+
try:
|
1354
|
+
subprocess.Popen([cmd, selected_file],
|
1355
|
+
start_new_session=True,
|
1356
|
+
shell=(os.name == 'nt'),
|
1357
|
+
stdout=devnull,
|
1358
|
+
stderr=devnull)
|
1359
|
+
time.sleep(0.3)
|
1360
|
+
self.flush_stdin()
|
1361
|
+
success = True
|
1362
|
+
break
|
1363
|
+
except FileNotFoundError:
|
1364
|
+
continue
|
1365
|
+
|
1366
|
+
if not success:
|
1367
|
+
print(f"{Fore.YELLOW} Notepad++ not found in PATH. Falling back to system notepad.")
|
1368
|
+
print(f"{Fore.YELLOW} TIP - Add the path of notepad++.exe file into the system's environment variable path.")
|
1369
|
+
print(f"{Fore.YELLOW} Till then Falling Back to the regulare notepad on Windows.")
|
1370
|
+
# Fall back to regular notepad on Windows
|
1371
|
+
subprocess.Popen(['notepad', selected_file],
|
1372
|
+
start_new_session=True,
|
1373
|
+
shell=True,
|
1374
|
+
stdout=devnull,
|
1375
|
+
stderr=devnull)
|
1376
|
+
time.sleep(0.3)
|
1377
|
+
self.flush_stdin()
|
1378
|
+
except:
|
1379
|
+
# If all else fails, use system default
|
1380
|
+
subprocess.Popen(['notepad', selected_file],
|
1381
|
+
start_new_session=True,
|
1382
|
+
shell=True,
|
1383
|
+
stdout=devnull,
|
1384
|
+
stderr=devnull) if os.name == 'nt' else subprocess.call(['xdg-open', selected_file], stdout=devnull, stderr=devnull)
|
1385
|
+
time.sleep(0.3)
|
1386
|
+
self.flush_stdin()
|
1387
|
+
elif editor == "atom":
|
1388
|
+
success = False
|
1389
|
+
try:
|
1390
|
+
subprocess.Popen(['atom', selected_file],
|
1391
|
+
start_new_session=True,
|
1392
|
+
shell=(os.name == 'nt'),
|
1393
|
+
stdout=devnull,
|
1394
|
+
stderr=devnull)
|
1395
|
+
time.sleep(0.3)
|
1396
|
+
self.flush_stdin()
|
1397
|
+
success = True
|
1398
|
+
except FileNotFoundError:
|
1399
|
+
print(f"{Fore.RED} File not found")
|
1400
|
+
|
1401
|
+
if not success:
|
1402
|
+
print(f"{Fore.YELLOW} Atom not found in PATH. Falling back to system default.")
|
1403
|
+
print(f"{Fore.YELLOW} TIP - Add the path of atom.exe file into the system's environment variable path.\n")
|
1404
|
+
# Fall back to system default
|
1405
|
+
if os.name == 'nt':
|
1406
|
+
subprocess.Popen(['notepad', selected_file],
|
1407
|
+
start_new_session=True,
|
1408
|
+
shell=True,
|
1409
|
+
stdout=devnull,
|
1410
|
+
stderr=devnull)
|
1411
|
+
time.sleep(0.3)
|
1412
|
+
self.flush_stdin()
|
1413
|
+
else:
|
1414
|
+
subprocess.call(['xdg-open', selected_file], stdout=devnull, stderr=devnull)
|
1415
|
+
time.sleep(0.3)
|
1416
|
+
self.flush_stdin()
|
1417
|
+
else:
|
1418
|
+
# Custom command
|
1419
|
+
success = False
|
1420
|
+
try:
|
1421
|
+
subprocess.Popen(['notepad', selected_file],
|
1422
|
+
start_new_session=True,
|
1423
|
+
shell=True,
|
1424
|
+
stdout=devnull,
|
1425
|
+
stderr=devnull)
|
1426
|
+
time.sleep(0.3)
|
1427
|
+
self.flush_stdin()
|
1428
|
+
success = True
|
1429
|
+
except FileNotFoundError:
|
1430
|
+
print(f"{Fore.RED} File not found ")
|
1431
|
+
except Exception as e:
|
1432
|
+
print(f"{Fore.YELLOW} Error executing custom command. Falling back to system default.")
|
1433
|
+
# Fall back to system default
|
1434
|
+
if os.name == 'nt':
|
1435
|
+
subprocess.Popen(['notepad', selected_file],
|
1436
|
+
start_new_session=True,
|
1437
|
+
shell=True,
|
1438
|
+
stdout=devnull,
|
1439
|
+
stderr=devnull)
|
1440
|
+
time.sleep(0.3)
|
1441
|
+
self.flush_stdin()
|
1442
|
+
success = True
|
1443
|
+
else:
|
1444
|
+
subprocess.call(['xdg-open', selected_file], stdout=devnull, stderr=devnull)
|
1445
|
+
time.sleep(0.3)
|
1446
|
+
self.flush_stdin()
|
1447
|
+
success = True
|
1448
|
+
|
1449
|
+
print(f"{Fore.GREEN} Opened file for editing: {selected_file}\n")
|
1450
|
+
return True
|
1451
|
+
except Exception as e:
|
1452
|
+
print(f"{Fore.RED} Error opening file: {e}")
|
1453
|
+
print(f"{Fore.YELLOW} Attempting to open with system default editor instead...")
|
1454
|
+
try:
|
1455
|
+
# Last resort - try with system default
|
1456
|
+
if os.name == 'nt': # Windows
|
1457
|
+
subprocess.Popen(['notepad', selected_file],
|
1458
|
+
start_new_session=True,
|
1459
|
+
shell=True,
|
1460
|
+
stdout=devnull,
|
1461
|
+
stderr=devnull)
|
1462
|
+
time.sleep(0.3)
|
1463
|
+
self.flush_stdin()
|
1464
|
+
elif os.name == 'posix': # Linux/Mac
|
1465
|
+
if sys.platform == 'darwin': # Mac
|
1466
|
+
subprocess.call(['open', selected_file], stdout=devnull, stderr=devnull)
|
1467
|
+
time.sleep(0.3)
|
1468
|
+
self.flush_stdin()
|
1469
|
+
else: # Linux
|
1470
|
+
subprocess.call(['xdg-open', selected_file])
|
1471
|
+
time.sleep(0.3)
|
1472
|
+
self.flush_stdin()
|
1473
|
+
print(f"{Fore.GREEN} Opened file with system default editor: {selected_file}\n")
|
1474
|
+
return True
|
1475
|
+
except Exception as e2:
|
1476
|
+
print(f"{Fore.RED} All attempts to open the file failed: {e2}")
|
1477
|
+
print(f"{Fore.YELLOW} Please manually open the file: {selected_file}\n")
|
1478
|
+
return False
|
1479
|
+
except Exception as e:
|
1480
|
+
print(f"{Fore.RED} Error opening file: {e}\n")
|
1481
|
+
return False
|
1482
|
+
|
1483
|
+
def _find_solution_file(self, problem_num):
|
1484
|
+
"""Find solution file for the given problem number"""
|
1485
|
+
if not problem_num or problem_num < 1 or problem_num > len(self.problems):
|
1486
|
+
return None
|
1487
|
+
|
1488
|
+
# Get problem code for the selected problem
|
1489
|
+
problem_code = self.problems[problem_num-1][0]
|
1490
|
+
|
1491
|
+
# First try to use the stored contest directory
|
1492
|
+
if self.contest_dir and os.path.exists(self.contest_dir):
|
1493
|
+
contest_dir = self.contest_dir
|
1494
|
+
else:
|
1495
|
+
# Get contest directory from config (set by solve function)
|
1496
|
+
contest_dir = self.config.get("current_contest_dir", "")
|
1497
|
+
|
1498
|
+
# If contest directory not found, try to determine it
|
1499
|
+
if not contest_dir or not os.path.exists(contest_dir):
|
1500
|
+
solution_path = self.config.get("solution_path", "")
|
1501
|
+
if not solution_path or not os.path.exists(solution_path):
|
1502
|
+
return None
|
1503
|
+
|
1504
|
+
# Try to extract contest ID from URL or use stored contest ID
|
1505
|
+
contest_id = self.contest_id
|
1506
|
+
if not contest_id:
|
1507
|
+
contest_id = "unknown"
|
1508
|
+
if self.tabs and self.browser.driver:
|
1509
|
+
current_url = self.browser.driver.current_url
|
1510
|
+
if "START" in current_url:
|
1511
|
+
try:
|
1512
|
+
contest_id = ''.join(filter(str.isdigit, current_url.split("START")[1][:4]))
|
1513
|
+
except:
|
1514
|
+
pass
|
1515
|
+
|
1516
|
+
contest_dir = os.path.join(solution_path, f"Starter {contest_id}")
|
1517
|
+
if not os.path.exists(contest_dir):
|
1518
|
+
return None
|
1519
|
+
|
1520
|
+
# Get preferred language extension
|
1521
|
+
extension = LANGUAGE_EXTENSIONS.get(self.config.get("preferred_language", ""), "")
|
1522
|
+
|
1523
|
+
# Try to find file with preferred extension
|
1524
|
+
if extension:
|
1525
|
+
filepath = os.path.join(contest_dir, f"{problem_code}{extension}")
|
1526
|
+
if os.path.exists(filepath):
|
1527
|
+
return filepath
|
1528
|
+
|
1529
|
+
# If not found or no extension specified, try all possible extensions
|
1530
|
+
for ext in LANGUAGE_EXTENSIONS.values():
|
1531
|
+
filepath = os.path.join(contest_dir, f"{problem_code}{ext}")
|
1532
|
+
if os.path.exists(filepath):
|
1533
|
+
return filepath
|
1534
|
+
|
1535
|
+
return None
|
1536
|
+
# ----------------------------------------------------------
|