diff --git a/backend/src/api/packet_scripting_api.py b/backend/src/api/packet_scripting_api.py index 728171d..6f00215 100644 --- a/backend/src/api/packet_scripting_api.py +++ b/backend/src/api/packet_scripting_api.py @@ -493,11 +493,7 @@ async def upload_script( except ValueError as e: logger.warning("Invalid name provided: %s", name) raise HTTPException(status_code=400, detail=str(e)) - - if not script.filename.endswith(".py"): - logger.warning("Upload rejected: script not .py (name=%s original=%s)", name, script.filename) - raise HTTPException(status_code=400, detail="only .py scripts allowed") - + spath = script_path_for(name) if os.path.exists(spath): logger.warning("Upload rejected: script with name already exists: %s", name) diff --git a/frontend/src/components/PythonEditor.tsx b/frontend/src/components/PythonEditor.tsx index 4fd589b..85a4180 100644 --- a/frontend/src/components/PythonEditor.tsx +++ b/frontend/src/components/PythonEditor.tsx @@ -1,76 +1,111 @@ +// src/components/PythonEditor.tsx import Prism from 'prismjs'; import 'prismjs/components/prism-python'; import 'prismjs/themes/prism.css'; -import { useCallback, useMemo } from 'react'; -import Editor from 'react-simple-code-editor'; +import React, { useEffect, useMemo, useRef } from 'react'; -// Props for the editor export type PythonEditorProps = { value: string; onChange: (code: string) => void; readOnly?: boolean; - className?: string; - height?: string | number; // e.g. '200px' or 200 - wrap?: boolean; // if true, long lines will wrap; default false (no wrap) + height?: string | number; + wrap?: boolean; + fontSize?: number; + tabSize?: number; }; +const DEFAULT_FONT = + '"JetBrains Mono", "Fira Code", "Source Code Pro", ui-monospace, SFMono-Regular, Menlo, Monaco, monospace'; + export default function PythonEditor({ value, onChange, readOnly = false, height = 480, - className = '', wrap = false, + fontSize = 14, + tabSize = 4, }: PythonEditorProps) { - const highlight = useCallback((code: string) => { - try { - return Prism.highlight(code, Prism.languages.python, 'python'); - } catch (e) { - return code; + const textareaRef = useRef(null); + const preRef = useRef(null); + + const h = typeof height === 'number' ? `${height}px` : height; + + const lineHeight = Math.round(fontSize * 1.6); + + // Highlight whenever value changes + useEffect(() => { + if (preRef.current) { + preRef.current.innerHTML = Prism.highlight(value, Prism.languages.python, 'python'); } - }, []); + }, [value]); - // container style: fixed height + scrolling - const containerStyle = useMemo(() => { - const h = typeof height === 'number' ? `${height}px` : height; - return { - height: h, - maxHeight: h, - overflow: 'auto' as const, // enable scrolling - position: 'relative' as const, - borderRadius: 6, - border: '1px solid rgba(0,0,0,0.06)', - background: 'var(--code-bg, white)', - }; - }, [height]); + // Scroll sync + const handleScroll = () => { + if (!textareaRef.current || !preRef.current) return; + preRef.current.scrollTop = textareaRef.current.scrollTop; + preRef.current.scrollLeft = textareaRef.current.scrollLeft; + }; - // editor style: fill container and use monospace font - const editorStyle = useMemo( + const sharedStyle: React.CSSProperties = useMemo( () => ({ - fontFamily: '"JetBrains Mono", "Fira Code", monospace', - fontSize: 14, - // make editor fill the container (pre/textarea inside will take up height) - minHeight: '100%', - boxSizing: 'border-box' as const, + fontFamily: DEFAULT_FONT, + fontSize: `${fontSize}px`, + lineHeight: `${lineHeight}px`, + tabSize, + whiteSpace: wrap ? 'pre-wrap' : 'pre', + wordBreak: 'break-word', padding: 12, - outline: 'none', - whiteSpace: wrap ? 'pre-wrap' : 'pre', // pre-wrap if wrap requested - wordBreak: 'break-word' as const, + boxSizing: 'border-box', }), - [wrap], + [fontSize, lineHeight, tabSize, wrap], ); return ( -
- + {/* Highlight layer */} +