diff --git a/pcreater.py b/pcreater.py new file mode 100644 index 0000000..df1b176 --- /dev/null +++ b/pcreater.py @@ -0,0 +1,569 @@ +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox, filedialog +import json +import os +from pathlib import Path +import tempfile +import webbrowser +try: + from tkinterweb import HtmlFrame # For markdown preview +except ImportError: + # Fallback if tkinterweb is not available + HtmlFrame = None + +# For syntax highlighting +try: + from pygments import lex + from pygments.lexers import PythonLexer + from pygments.styles import get_style_by_name + PYGMENTS_AVAILABLE = True +except ImportError: + PYGMENTS_AVAILABLE = False + +class CodeEditor(scrolledtext.ScrolledText): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bind('', self._tab) + self.bind('', self._shift_tab) + self.bind('', self._on_key_release) + self.configure(font=('Consolas', 10)) + + # Setup tags for syntax highlighting if available + if PYGMENTS_AVAILABLE: + self._setup_tags() + + def _tab(self, event): + self.insert(tk.INSERT, " " * 4) + return "break" # Prevent default behavior + + def _shift_tab(self, event): + # Remove indentation + sel_start = self.index(tk.SEL_FIRST) + sel_end = self.index(tk.SEL_LAST) + + if sel_start and sel_end: + # Get the lines in the selection + start_line = int(sel_start.split('.')[0]) + end_line = int(sel_end.split('.')[0]) + + for line_num in range(start_line, end_line + 1): + line_start = f"{line_num}.0" + line_end = f"{line_num}.end" + line_text = self.get(line_start, line_end) + + if line_text.startswith(" " * 4): + self.delete(line_start, f"{line_num}.4") + elif line_text.startswith("\t"): + self.delete(line_start, f"{line_num}.1") + + return "break" + + def _on_key_release(self, event): + if event.keysym == 'Return': + # Auto-indent new line + current_line = self.get("insert linestart", "insert") + indent = len(current_line) - len(current_line.lstrip()) + self.insert("insert", " " * indent) + + # Syntax highlighting if available + if PYGMENTS_AVAILABLE: + self.highlight_syntax() + + def _setup_tags(self): + try: + self.style = get_style_by_name('default') + + # Create tags for different token types + for token, style in self.style: + color = style.get('color', '') + if color and self._is_valid_color(color): + self.tag_configure(str(token), foreground=f'#{color}') + except Exception as e: + print(f"Error setting up syntax highlighting: {e}") + + def _is_valid_color(self, color_str): + """Check if a string is a valid hex color""" + if not color_str: + return False + # Remove # if present + if color_str.startswith('#'): + color_str = color_str[1:] + # Check if it's a valid hex color (3 or 6 digits) + if len(color_str) not in (3, 6): + return False + try: + int(color_str, 16) + return True + except ValueError: + return False + + def highlight_syntax(self): + if not PYGMENTS_AVAILABLE: + return + + try: + # Remove previous highlighting + for tag in self.tag_names(): + if tag != "sel": + self.tag_remove(tag, "1.0", "end") + + # Get the text + code = self.get("1.0", "end-1c") + + # Lex the code and apply tags + pos = 0 + for token, text in lex(code, PythonLexer()): + if text.strip(): # Only apply tags to non-whitespace text + start = f"1.0+{pos}c" + end = f"1.0+{pos+len(text)}c" + self.tag_add(str(token), start, end) + pos += len(text) + except Exception as e: + print(f"Error during syntax highlighting: {e}") + +class MarkdownEditor: + def __init__(self, parent): + self.parent = parent + + # Create a paned window for split view + self.paned = ttk.PanedWindow(parent, orient=tk.HORIZONTAL) + self.paned.pack(fill=tk.BOTH, expand=True) + + # Left side - text editor + self.editor_frame = ttk.Frame(self.paned) + self.editor = scrolledtext.ScrolledText( + self.editor_frame, wrap=tk.WORD, font=('Consolas', 10) + ) + self.editor.pack(fill=tk.BOTH, expand=True) + + # Right side - preview (if available) + self.preview_frame = ttk.Frame(self.paned) + if HtmlFrame: + self.preview = HtmlFrame(self.preview_frame) + self.preview.pack(fill=tk.BOTH, expand=True) + + # Add both frames to the paned window + self.paned.add(self.editor_frame, weight=1) + self.paned.add(self.preview_frame, weight=1) + + # Bind key events + self.editor.bind('', self.update_preview) + else: + # If tkinterweb is not available, just show the editor + self.paned.add(self.editor_frame, weight=1) + self.preview = None + + def update_preview(self, event=None): + if not self.preview: + return + + # Get the markdown text + markdown_text = self.editor.get("1.0", "end-1c") + + # Convert to HTML (simple conversion for demonstration) + # In a real application, you might want to use a markdown library + html_content = f""" + + + + + + {self._simple_markdown_to_html(markdown_text)} + + + """ + + # Update the preview + self.preview.load_html(html_content) + + def _simple_markdown_to_html(self, text): + # Very basic markdown to HTML conversion + # For a real application, consider using a proper markdown library + html = text + html = html.replace("# ", "

").replace("\n# ", "

\n

") + "

" + html = html.replace("## ", "

").replace("\n## ", "

\n

") + "

" + html = html.replace("### ", "

").replace("\n### ", "

\n

") + "

" + html = html.replace("**", "").replace("**", "") + html = html.replace("*", "").replace("*", "") + html = html.replace("`", "").replace("`", "") + html = html.replace("\n", "
") + return html + + def get(self, start, end): + return self.editor.get(start, end) + + def insert(self, index, text): + self.editor.insert(index, text) + if self.preview: + self.update_preview() + + def delete(self, start, end): + self.editor.delete(start, end) + if self.preview: + self.update_preview() + +class ProblemCreatorApp: + def __init__(self, root): + self.root = root + self.root.title("Coding Problem Creator") + self.root.geometry("1000x800") + self.root.minsize(800, 600) + + # Configure style + style = ttk.Style() + style.theme_use('clam') + + self.create_widgets() + + def create_widgets(self): + # Create notebook for tabs + self.notebook = ttk.Notebook(self.root) + self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Main info tab + self.main_frame = ttk.Frame(self.notebook, padding="10") + self.notebook.add(self.main_frame, text="Problem Info") + + # Configure grid weights for responsiveness + self.main_frame.columnconfigure(1, weight=1) + + # Title + title_label = ttk.Label(self.main_frame, text="Coding Problem Creator", + font=('Arial', 16, 'bold')) + title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20)) + + # Problem Name + ttk.Label(self.main_frame, text="Problem Name:", font=('Arial', 10, 'bold')).grid( + row=1, column=0, sticky=tk.W, pady=(0, 5)) + self.problem_name = ttk.Entry(self.main_frame, width=40, font=('Arial', 10)) + self.problem_name.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=(0, 10)) + + # Difficulty + ttk.Label(self.main_frame, text="Difficulty:", font=('Arial', 10, 'bold')).grid( + row=2, column=0, sticky=tk.W, pady=(0, 5)) + self.difficulty = ttk.Combobox(self.main_frame, values=["easy", "medium", "hard"], + state="readonly", width=15) + self.difficulty.set("medium") + self.difficulty.grid(row=2, column=1, sticky=tk.W, pady=(0, 10)) + + # Description tab + self.desc_frame = ttk.Frame(self.notebook) + self.notebook.add(self.desc_frame, text="Description") + + # Create markdown editor for description + self.description = MarkdownEditor(self.desc_frame) + + # Test Code tab + self.test_frame = ttk.Frame(self.notebook) + self.notebook.add(self.test_frame, text="Test Code") + + # Create code editor for test code + self.test_code = CodeEditor(self.test_frame, width=80, height=20) + self.test_code.pack(fill=tk.BOTH, expand=True) + + # Insert template code + template_code = '''import unittest + +class TestCase(unittest.TestCase): + def test(self): + # Write your test cases here + # Example: + # from solution import your_function + # self.assertEqual(your_function("input"), "expected_output") + pass''' + + self.test_code.insert("1.0", template_code) + if PYGMENTS_AVAILABLE: + self.test_code.highlight_syntax() + + # Buttons frame at the bottom + button_frame = ttk.Frame(self.root) + button_frame.pack(fill=tk.X, padx=10, pady=10) + + create_button = ttk.Button(button_frame, text="Create Problem", + command=self.create_problem) + create_button.pack(side=tk.LEFT, padx=(0, 10)) + + clear_button = ttk.Button(button_frame, text="Clear All", + command=self.clear_all) + clear_button.pack(side=tk.LEFT, padx=(0, 10)) + + load_button = ttk.Button(button_frame, text="Load Existing", + command=self.load_existing) + load_button.pack(side=tk.LEFT) + + # Status bar + self.status_var = tk.StringVar() + self.status_var.set("Ready to create a new problem...") + status_bar = ttk.Label(self.root, textvariable=self.status_var, + relief=tk.SUNKEN, anchor=tk.W) + status_bar.pack(fill=tk.X, padx=10, pady=(0, 10)) + + def show_help(self): + help_text = """Test Code Help: + +Your test code should follow this structure: + +import unittest + +class TestCase(unittest.TestCase): + def test(self): + # Import your solution function + from solution import your_function_name + + # Write test assertions + self.assertEqual(your_function_name(input), expected_output) + self.assertTrue(condition) + self.assertFalse(condition) + +Tips: +- Replace 'your_function_name' with the actual function name +- Add multiple test cases with different inputs +- Use descriptive variable names +- Test edge cases (empty inputs, large inputs, etc.) + +Example for a palindrome checker: + from solution import is_palindrome + self.assertTrue(is_palindrome("racecar")) + self.assertFalse(is_palindrome("hello")) + self.assertTrue(is_palindrome("")) +""" + messagebox.showinfo("Test Code Help", help_text) + + def validate_inputs(self): + if not self.problem_name.get().strip(): + messagebox.showerror("Error", "Problem name is required!") + return False + + if not self.description.get("1.0", tk.END).strip(): + messagebox.showerror("Error", "Description is required!") + return False + + test_code = self.test_code.get("1.0", tk.END).strip() + if not test_code or test_code == "pass" or len(test_code) < 50: + messagebox.showerror("Error", "Please provide proper test code!") + return False + + # Validate problem name (should be filesystem-safe) + name = self.problem_name.get().strip() + if not name.replace("_", "").replace("-", "").replace(" ", "").isalnum(): + messagebox.showerror("Error", "Problem name should only contain letters, numbers, spaces, hyphens, and underscores!") + return False + + return True + + def create_problem(self): + if not self.validate_inputs(): + return + + try: + # Get values + problem_name = self.problem_name.get().strip() + description_text = self.description.get("1.0", tk.END).strip() + difficulty = self.difficulty.get() + test_code = self.test_code.get("1.0", tk.END).strip() + + # Create safe folder name (replace spaces with underscores) + folder_name = problem_name.replace(" ", "_") + + # Create directory structure + base_path = Path("src/problems") + problem_path = base_path / folder_name + + # Create directories if they don't exist + problem_path.mkdir(parents=True, exist_ok=True) + + # Create manifest.json + manifest = { + "title": problem_name, + "description": description_text, + "description_md": f"problems/{folder_name}/description.md", + "test_code": f"problems/{folder_name}/test.py", + "difficulty": difficulty + } + + manifest_path = problem_path / "manifest.json" + with open(manifest_path, 'w', encoding='utf-8') as f: + json.dump(manifest, f, indent=4, ensure_ascii=False) + + # Create description.md + description_md_path = problem_path / "description.md" + with open(description_md_path, 'w', encoding='utf-8') as f: + f.write(description_text) + + # Create test.py + test_py_path = problem_path / "test.py" + with open(test_py_path, 'w', encoding='utf-8') as f: + f.write(test_code) + + # Create empty solution.py template + solution_py_path = problem_path / "solution.py" + if not solution_py_path.exists(): + with open(solution_py_path, 'w', encoding='utf-8') as f: + f.write(f"# {problem_name} Solution\n") + f.write("# Implement your solution here\n\n") + f.write("def your_function():\n") + f.write(" pass\n") + + self.status_var.set(f"✓ Problem '{problem_name}' created successfully in {problem_path}") + + result = messagebox.askyesno("Success", + f"Problem '{problem_name}' created successfully!\n\n" + f"Location: {problem_path}\n\n" + "Would you like to open the folder?") + + if result: + self.open_folder(problem_path) + + except Exception as e: + error_msg = f"Error creating problem: {str(e)}" + self.status_var.set(error_msg) + messagebox.showerror("Error", error_msg) + + def open_folder(self, path): + """Cross-platform folder opening""" + import subprocess + import platform + + try: + if platform.system() == "Windows": + os.startfile(path) + elif platform.system() == "Darwin": # macOS + subprocess.run(["open", path]) + else: # Linux and other Unix-like + subprocess.run(["xdg-open", path]) + except Exception as e: + messagebox.showwarning("Warning", f"Could not open folder: {str(e)}") + + def clear_all(self): + result = messagebox.askyesno("Confirm", "Clear all fields?") + if result: + self.problem_name.delete(0, tk.END) + self.description.delete("1.0", tk.END) + self.difficulty.set("medium") + self.test_code.delete("1.0", tk.END) + # Re-insert template + template_code = '''import unittest + +class TestCase(unittest.TestCase): + def test(self): + # Write your test cases here + # Example: + # from solution import your_function + # self.assertEqual(your_function("input"), "expected_output") + pass''' + self.test_code.insert("1.0", template_code) + if PYGMENTS_AVAILABLE: + self.test_code.highlight_syntax() + self.status_var.set("All fields cleared.") + + def load_existing(self): + try: + base_path = Path("src/problems") + if not base_path.exists(): + messagebox.showwarning("Warning", "No problems directory found!") + return + + # Get list of existing problems + problems = [d.name for d in base_path.iterdir() if d.is_dir()] + + if not problems: + messagebox.showinfo("Info", "No existing problems found!") + return + + # Create selection dialog + dialog = tk.Toplevel(self.root) + dialog.title("Load Existing Problem") + dialog.geometry("400x300") + dialog.transient(self.root) + dialog.grab_set() + + ttk.Label(dialog, text="Select a problem to load:", + font=('Arial', 10, 'bold')).pack(pady=10) + + # Listbox with scrollbar + frame = ttk.Frame(dialog) + frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) + + scrollbar = ttk.Scrollbar(frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + listbox = tk.Listbox(frame, yscrollcommand=scrollbar.set) + listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.config(command=listbox.yview) + + for problem in sorted(problems): + listbox.insert(tk.END, problem) + + def load_selected(): + selection = listbox.curselection() + if not selection: + messagebox.showwarning("Warning", "Please select a problem!") + return + + selected_problem = problems[selection[0]] + self.load_problem_data(selected_problem) + dialog.destroy() + + button_frame = ttk.Frame(dialog) + button_frame.pack(pady=10) + + ttk.Button(button_frame, text="Load", command=load_selected).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.LEFT, padx=5) + + except Exception as e: + messagebox.showerror("Error", f"Error loading existing problems: {str(e)}") + + def load_problem_data(self, problem_name): + try: + problem_path = Path("src/problems") / problem_name + manifest_path = problem_path / "manifest.json" + test_path = problem_path / "test.py" + desc_path = problem_path / "description.md" + + # Load manifest + with open(manifest_path, 'r', encoding='utf-8') as f: + manifest = json.load(f) + + # Load test code + test_code = "" + if test_path.exists(): + with open(test_path, 'r', encoding='utf-8') as f: + test_code = f.read() + + # Load description + description = "" + if desc_path.exists(): + with open(desc_path, 'r', encoding='utf-8') as f: + description = f.read() + + # Populate fields + self.problem_name.delete(0, tk.END) + self.problem_name.insert(0, manifest["title"]) + + self.description.delete("1.0", tk.END) + self.description.insert("1.0", description) + + self.difficulty.set(manifest["difficulty"]) + + self.test_code.delete("1.0", tk.END) + self.test_code.insert("1.0", test_code) + if PYGMENTS_AVAILABLE: + self.test_code.highlight_syntax() + + self.status_var.set(f"Loaded problem: {problem_name}") + + except Exception as e: + messagebox.showerror("Error", f"Error loading problem data: {str(e)}") + +def main(): + root = tk.Tk() + app = ProblemCreatorApp(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 26a9044..8233b47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ Markdown>=3.6 MarkupSafe>=2.1 watchdog>=4.0 gunicorn>=23.0.0 -waitress>=3.0.2 \ No newline at end of file +waitress>=3.0.2 +tkinterweb_html>=1.1.4 +pillow>=11.3.0 \ No newline at end of file