├── public ├── robots.txt ├── images │ └── logo.webp ├── icons │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── icon512_maskable.png │ ├── icon512_rounded.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png └── site.webmanifest ├── src ├── vite-env.d.ts ├── lib │ ├── utils.ts │ └── url.ts ├── main.tsx ├── components │ ├── loader.tsx │ ├── ui │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── resizable.tsx │ │ ├── button.tsx │ │ └── drawer.tsx │ ├── editor │ │ ├── editor.tsx │ │ ├── stats.tsx │ │ └── terminal.tsx │ ├── nav-buttons.tsx │ └── settings.tsx ├── types.ts ├── globals.css ├── App.tsx └── store │ └── useStore.ts ├── bun.lockb ├── postcss.config.js ├── tsconfig.json ├── .gitignore ├── prettier.config.js ├── tsconfig.node.json ├── components.json ├── .github └── workflows │ ├── lint.yml │ ├── format.yml │ └── codeql.yml ├── .eslintrc.cjs ├── tsconfig.app.json ├── vite.config.ts ├── LICENSE ├── package.json ├── tailwind.config.js ├── README.md └── index.html /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/bun.lockb -------------------------------------------------------------------------------- /public/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/images/logo.webp -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/icon512_maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/icon512_maskable.png -------------------------------------------------------------------------------- /public/icons/icon512_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/icon512_rounded.png -------------------------------------------------------------------------------- /public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vwh/python-playground/HEAD/public/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/url.ts: -------------------------------------------------------------------------------- 1 | export const getDecodedParam = (param: string | null): string | null => { 2 | if (param) { 3 | try { 4 | return atob(param); 5 | } catch (e) { 6 | console.error("Failed to decode parameter:", e); 7 | return null; 8 | } 9 | } 10 | return null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import "@/globals.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | 6 | import App from "@/App.tsx"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["./src/*"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | const config = { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | arrowParens: "always", 5 | printWidth: 80, 6 | singleQuote: false, 7 | jsxSingleQuote: false, 8 | semi: true, 9 | trailingComma: "none", 10 | tabWidth: 2, 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /src/components/loader.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircleIcon } from "lucide-react"; 2 | 3 | interface LoaderProps { 4 | text: string; 5 | } 6 | 7 | export default function Loader({ text }: LoaderProps) { 8 | return ( 9 |
10 | 11 |

{text}

12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface PyodideInterface { 2 | runPythonAsync: (code: string) => Promise; 3 | loadPackage: (name: string) => Promise; 4 | globals: { 5 | set: (key: string, value: any) => void; 6 | }; 7 | pyimport: (name: string) => Promise; 8 | } 9 | 10 | declare global { 11 | interface Window { 12 | loadPyodide: () => Promise; 13 | micropip: { 14 | install: ( 15 | packages: string | string[], 16 | keep_going?: boolean 17 | ) => Promise; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: "18" 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Run ESLint 28 | run: npm run lint 29 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Check Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | format: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: "16" 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Run Prettier check 28 | run: npm run format:check 29 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | env: { browser: true, es2020: true }, 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:react-hooks/recommended" 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs"], 11 | parser: "@typescript-eslint/parser", 12 | plugins: ["react-refresh"], 13 | rules: { 14 | "react-hooks/exhaustive-deps": "off", 15 | "eslint:@typescript-eslint/no-explicit-any": "off", 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "react-refresh/only-export-components": [ 18 | "warn", 19 | { allowConstantExport: true } 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#1e1e1e", 3 | "background_color": "#141110", 4 | "icons": [ 5 | { 6 | "purpose": "maskable", 7 | "sizes": "512x512", 8 | "src": "./icons/icon512_maskable.png", 9 | "type": "image/png" 10 | }, 11 | { 12 | "purpose": "any", 13 | "sizes": "512x512", 14 | "src": "./icons/icon512_rounded.png", 15 | "type": "image/png" 16 | } 17 | ], 18 | "orientation": "any", 19 | "display": "standalone", 20 | "dir": "left", 21 | "lang": "en-US", 22 | "start_url": "/python-playground/", 23 | "scope": "/python-playground/", 24 | "description": "Explore and experiment with Python code", 25 | "id": "vwh-python-playground", 26 | "name": "Python Playground", 27 | "short_name": "Python" 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | "baseUrl": ".", 27 | "paths": { 28 | "@/*": ["./src/*"] 29 | } 30 | }, 31 | 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | } 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | 5 | import compression from "vite-plugin-compression"; 6 | import dynamicImport from "vite-plugin-dynamic-import"; 7 | import { VitePWA } from "vite-plugin-pwa"; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | base: "/python-playground/", 12 | plugins: [ 13 | compression(), 14 | react(), 15 | dynamicImport(), 16 | VitePWA({ registerType: "autoUpdate" }) 17 | ], 18 | build: { 19 | rollupOptions: { 20 | output: { 21 | manualChunks(id) { 22 | if (id.includes("node_modules")) { 23 | return id 24 | .toString() 25 | .split("node_modules/")[1] 26 | .split("/")[0] 27 | .toString(); 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | resolve: { 34 | alias: { 35 | "@": path.resolve(__dirname, "./src") 36 | } 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 VWH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/editor/editor.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { useStore } from "@/store/useStore"; 3 | 4 | import MonacoEditor, { type OnMount } from "@monaco-editor/react"; 5 | import Loader from "@/components/loader"; 6 | 7 | export default function Editor() { 8 | const { code, setCode } = useStore(); 9 | 10 | const handleCodeOnChange = useCallback( 11 | (value: string | undefined) => { 12 | if (value) { 13 | setCode(value); 14 | } 15 | }, 16 | [setCode] 17 | ); 18 | 19 | const editorOptions = useMemo( 20 | () => ({ 21 | minimap: { enabled: false }, 22 | scrollBeyondLastLine: false, 23 | fontSize: 14, 24 | fontFamily: "'Fira Code', monospace", 25 | fontLigatures: true, 26 | cursorSmoothCaretAnimation: "on", 27 | smoothScrolling: true, 28 | padding: { top: 16, bottom: 16 }, 29 | renderLineHighlight: "all", 30 | matchBrackets: "always", 31 | autoClosingBrackets: "always", 32 | autoClosingQuotes: "always", 33 | formatOnPaste: true, 34 | formatOnType: true 35 | }), 36 | [] 37 | ); 38 | 39 | const handleEditorDidMount: OnMount = useCallback((editor) => { 40 | editor.focus(); 41 | }, []); 42 | 43 | return ( 44 |
45 | } 51 | // @ts-expect-error ts(2322) 52 | options={editorOptions} 53 | onMount={handleEditorDidMount} 54 | className="h-full w-full" 55 | /> 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/editor/stats.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useStore } from "@/store/useStore"; 3 | 4 | import { FileTextIcon, TypeIcon, HashIcon } from "lucide-react"; 5 | 6 | interface CodeStats { 7 | lines: number; 8 | words: number; 9 | characters: number; 10 | } 11 | 12 | export default function Stats() { 13 | const { code } = useStore(); 14 | 15 | const stats: CodeStats = useMemo( 16 | () => ({ 17 | lines: code.split("\n").length, 18 | words: code.trim().split(/\s+/).length, 19 | characters: code.length 20 | }), 21 | [code] 22 | ); 23 | 24 | return ( 25 |
26 |
27 | } 29 | value={stats.lines} 30 | label="Lines" 31 | /> 32 | } 34 | value={stats.words} 35 | label="Words" 36 | /> 37 | } 39 | value={stats.characters} 40 | label="Chars" 41 | /> 42 |
43 |
44 | ); 45 | } 46 | 47 | interface StatItemProps { 48 | icon: React.ReactNode; 49 | value: number; 50 | label: string; 51 | } 52 | 53 | function StatItem({ icon, value, label }: StatItemProps) { 54 | return ( 55 |
56 |
57 | {icon} 58 | {value} 59 |
60 | {label} 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import * as ResizablePrimitive from "react-resizable-panels"; 2 | import { GripVerticalIcon } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const ResizablePanelGroup = ({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 17 | ); 18 | 19 | const ResizablePanel = ResizablePrimitive.Panel; 20 | 21 | const ResizableHandle = ({ 22 | withHandle, 23 | className, 24 | ...props 25 | }: React.ComponentProps & { 26 | withHandle?: boolean; 27 | }) => ( 28 | div]:rotate-90", 31 | className 32 | )} 33 | {...props} 34 | > 35 | {withHandle && ( 36 |
37 | 38 |
39 | )} 40 |
41 | ); 42 | 43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 44 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline" 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10" 27 | } 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default" 32 | } 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://vwh.github.io/python-playground/", 3 | "name": "python-playground", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview", 10 | "deploy": "gh-pages -d dist", 11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", 13 | "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/vwh/python-playground.git" 18 | }, 19 | "author": "vwh", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@monaco-editor/react": "^4.6.0", 23 | "@radix-ui/react-dialog": "^1.1.4", 24 | "@radix-ui/react-separator": "^1.1.1", 25 | "@radix-ui/react-slot": "^1.1.1", 26 | "class-variance-authority": "^0.7.1", 27 | "clsx": "^2.1.1", 28 | "error-stack-parser": "^2.1.4", 29 | "lucide-react": "^0.424.0", 30 | "react": "^18.3.1", 31 | "react-dom": "^18.3.1", 32 | "react-resizable-panels": "^2.1.7", 33 | "tailwind-merge": "^2.6.0", 34 | "tailwindcss-animate": "^1.0.7", 35 | "vaul": "^0.9.9", 36 | "zustand": "^4.5.5" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^22.10.5", 40 | "@types/react": "^18.3.18", 41 | "@types/react-dom": "^18.3.5", 42 | "@typescript-eslint/eslint-plugin": "^8.19.0", 43 | "@typescript-eslint/parser": "^8.19.0", 44 | "@vitejs/plugin-react": "^4.3.4", 45 | "autoprefixer": "^10.4.20", 46 | "eslint": "^8.57.1", 47 | "eslint-plugin-react-hooks": "^4.6.2", 48 | "eslint-plugin-react-refresh": "^0.4.16", 49 | "gh-pages": "^6.3.0", 50 | "postcss": "^8.4.49", 51 | "prettier": "^3.4.2", 52 | "prettier-plugin-tailwindcss": "^0.6.9", 53 | "tailwindcss": "^3.4.17", 54 | "typescript": "^5.7.2", 55 | "vite": "^5.4.11", 56 | "vite-plugin-compression": "^0.5.1", 57 | "vite-plugin-dynamic-import": "^1.6.0", 58 | "vite-plugin-pwa": "^0.20.5" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 222.2 84% 4.9%; 8 | --foreground: 210 40% 98%; 9 | --card: 222.2 84% 4.9%; 10 | --card-foreground: 210 40% 98%; 11 | --popover: 222.2 84% 4.9%; 12 | --popover-foreground: 210 40% 98%; 13 | --primary: 210 40% 98%; 14 | --primary-foreground: 222.2 47.4% 11.2%; 15 | --secondary: 217.2 32.6% 17.5%; 16 | --secondary-foreground: 210 40% 98%; 17 | --muted: 217.2 32.6% 17.5%; 18 | --muted-foreground: 215 20.2% 65.1%; 19 | --accent: 217.2 32.6% 17.5%; 20 | --accent-foreground: 210 40% 98%; 21 | --destructive: 0 62.8% 30.6%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 217.2 32.6% 17.5%; 24 | --input: 217.2 32.6% 17.5%; 25 | --ring: 212.7 26.8% 83.9; 26 | --radius: 0.5rem; 27 | } 28 | } 29 | 30 | @layer base { 31 | * { 32 | @apply border-border; 33 | } 34 | body { 35 | @apply bg-background text-foreground; 36 | font-feature-settings: 37 | "rlig" 1, 38 | "calt" 1; 39 | } 40 | 41 | /* For Webkit browsers like Chrome and Safari */ 42 | ::-webkit-scrollbar { 43 | width: 8px; 44 | height: 8px; 45 | } 46 | 47 | ::-webkit-scrollbar-track { 48 | background: var(--scrollbar-track); 49 | border-radius: 1px; 50 | } 51 | 52 | ::-webkit-scrollbar-thumb { 53 | background: var(--scrollbar-thumb); 54 | border-radius: 1px; 55 | transition: background 0.2s ease-in-out; 56 | } 57 | 58 | ::-webkit-scrollbar-thumb:hover { 59 | background: var(--scrollbar-thumb-hover); 60 | } 61 | 62 | :root { 63 | --scrollbar-track: hsl(var(--secondary)); 64 | --scrollbar-thumb: hsl(var(--muted-foreground) / 0.5); 65 | --scrollbar-thumb-hover: hsl(var(--muted-foreground) / 0.7); 66 | } 67 | } 68 | 69 | .markdown-body pre { 70 | direction: ltr; 71 | @apply rounded border bg-background !important; 72 | } 73 | 74 | .markdown-body { 75 | background: none !important; 76 | } 77 | 78 | .monaco-editor .minimap, 79 | .iPadShowKeyboard { 80 | display: none; 81 | } 82 | 83 | .monaco-editor-background { 84 | background-color: hsl(var(--background)) !important; 85 | } 86 | 87 | .margin-view-overlays { 88 | background-color: hsl(var(--background)) !important; 89 | } 90 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useStore } from "@/store/useStore"; 3 | 4 | import { getDecodedParam } from "@/lib/url"; 5 | 6 | import Editor from "@/components/editor/editor"; 7 | import ButtonsNav from "@/components/nav-buttons"; 8 | import Stats from "@/components/editor/stats"; 9 | import Terminal from "@/components/editor/terminal"; 10 | import { 11 | ResizableHandle, 12 | ResizablePanel, 13 | ResizablePanelGroup 14 | } from "@/components/ui/resizable"; 15 | 16 | export default function App() { 17 | const { direction, setCode, initializePyodide, isPyodideLoading } = 18 | useStore(); 19 | 20 | // Initialize Pyodide and ( set the code from URL params if present ) 21 | useEffect(() => { 22 | const initializeApp = async () => { 23 | await initializePyodide(); 24 | 25 | const urlParams = new URLSearchParams(window.location.search); 26 | const encodedParam = urlParams.get("v"); 27 | if (encodedParam) { 28 | const decodedCode = getDecodedParam(encodedParam); 29 | if (decodedCode) { 30 | setCode(decodedCode); 31 | } 32 | } 33 | }; 34 | 35 | initializeApp(); 36 | }, [initializePyodide, setCode]); 37 | 38 | if (isPyodideLoading) { 39 | return ; 40 | } 41 | 42 | return ( 43 |
44 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | ); 60 | } 61 | 62 | function LoadingScreen() { 63 | return ( 64 |
65 |
66 | logo 71 |

72 | Loading WebAssembly 73 |

74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import tailwindcssAnimate from "tailwindcss-animate"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | darkMode: ["class"], 6 | content: [ 7 | "./pages/**/*.{ts,tsx}", 8 | "./components/**/*.{ts,tsx}", 9 | "./app/**/*.{ts,tsx}", 10 | "./src/**/*.{ts,tsx}" 11 | ], 12 | prefix: "", 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: "2rem", 17 | screens: { 18 | "2xl": "1400px" 19 | } 20 | }, 21 | extend: { 22 | borderWidth: { 23 | DEFAULT: "2px" 24 | }, 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))" 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))" 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))" 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))" 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))" 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))" 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))" 58 | } 59 | }, 60 | borderRadius: { 61 | lg: "var(--radius)", 62 | md: "calc(var(--radius) - 2px)", 63 | sm: "calc(var(--radius) - 4px)" 64 | }, 65 | keyframes: { 66 | "accordion-down": { 67 | from: { height: "0" }, 68 | to: { height: "var(--radix-accordion-content-height)" } 69 | }, 70 | "accordion-up": { 71 | from: { height: "var(--radix-accordion-content-height)" }, 72 | to: { height: "0" } 73 | }, 74 | ripple: { 75 | "0%, 100%": { 76 | transform: "translate(-50%, -50%) scale(1)" 77 | }, 78 | "50%": { 79 | transform: "translate(-50%, -50%) scale(0.9)" 80 | } 81 | } 82 | }, 83 | animation: { 84 | "accordion-down": "accordion-down 0.2s ease-out", 85 | "accordion-up": "accordion-up 0.2s ease-out", 86 | ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite" 87 | } 88 | } 89 | }, 90 | plugins: [tailwindcssAnimate] 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/nav-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, memo } from "react"; 2 | import { useStore } from "@/store/useStore"; 3 | 4 | import { Button } from "./ui/button"; 5 | import Settings from "./settings"; 6 | 7 | import { 8 | ReplaceIcon, 9 | PlayIcon, 10 | Trash2Icon, 11 | LoaderCircleIcon 12 | } from "lucide-react"; 13 | 14 | const ButtonsNav = () => { 15 | const { 16 | setDirection, 17 | direction, 18 | clearOutput, 19 | setError, 20 | runCode, 21 | code, 22 | isCodeExecuting 23 | } = useStore(); 24 | 25 | const handleDirectionChange = useCallback(() => { 26 | setDirection(direction === "vertical" ? "horizontal" : "vertical"); 27 | }, [direction, setDirection]); 28 | 29 | const handleTerminalClear = useCallback(() => { 30 | clearOutput("Running Python 3.12.7"); 31 | setError(null); 32 | }, [clearOutput, setError]); 33 | 34 | const handleRunCode = useCallback(() => { 35 | runCode(code); 36 | }, [runCode, code]); 37 | 38 | return ( 39 | 76 | ); 77 | }; 78 | 79 | const NavButton = memo( 80 | ({ 81 | onClick, 82 | disabled, 83 | icon, 84 | label, 85 | title 86 | }: { 87 | onClick: () => void; 88 | disabled?: boolean; 89 | icon: React.ReactNode; 90 | label: string; 91 | title: string; 92 | }) => ( 93 | 103 | ) 104 | ); 105 | 106 | NavButton.displayName = "NavButton"; 107 | 108 | export default memo(ButtonsNav); 109 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Drawer as DrawerPrimitive } from "vaul"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Drawer = ({ 7 | shouldScaleBackground = true, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ); 15 | Drawer.displayName = "Drawer"; 16 | 17 | const DrawerTrigger = DrawerPrimitive.Trigger; 18 | 19 | const DrawerPortal = DrawerPrimitive.Portal; 20 | 21 | const DrawerClose = DrawerPrimitive.Close; 22 | 23 | const DrawerOverlay = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 34 | 35 | const DrawerContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 |
50 | {children} 51 | 52 | 53 | )); 54 | DrawerContent.displayName = "DrawerContent"; 55 | 56 | const DrawerHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
64 | ); 65 | DrawerHeader.displayName = "DrawerHeader"; 66 | 67 | const DrawerFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
75 | ); 76 | DrawerFooter.displayName = "DrawerFooter"; 77 | 78 | const DrawerTitle = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )); 91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 92 | 93 | const DrawerDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )); 103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 104 | 105 | export { 106 | Drawer, 107 | DrawerPortal, 108 | DrawerOverlay, 109 | DrawerTrigger, 110 | DrawerClose, 111 | DrawerContent, 112 | DrawerHeader, 113 | DrawerFooter, 114 | DrawerTitle, 115 | DrawerDescription 116 | }; 117 | -------------------------------------------------------------------------------- /src/components/editor/terminal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, useCallback, useMemo } from "react"; 2 | import { useStore } from "@/store/useStore"; 3 | 4 | import Loader from "@/components/loader"; 5 | 6 | interface CommandHandlers { 7 | [key: string]: (input: string) => Promise; 8 | } 9 | 10 | export default function Terminal() { 11 | const { 12 | output, 13 | error, 14 | setOutput, 15 | isPyodideLoading, 16 | runCode, 17 | pipInstall, 18 | setError, 19 | clearOutput 20 | } = useStore(); 21 | const [terminalCode, setTerminalCode] = useState(""); 22 | const outputRef = useRef(null); 23 | 24 | const scrollToBottom = useCallback(() => { 25 | if (outputRef.current) { 26 | outputRef.current.scrollTop = outputRef.current.scrollHeight; 27 | } 28 | }, []); 29 | 30 | // Scroll to bottom on output change 31 | useEffect(() => { 32 | scrollToBottom(); 33 | }, [output, scrollToBottom]); 34 | 35 | const getCwd = useCallback(async () => { 36 | await runCode("import os; print(os.getcwd())"); 37 | }, [runCode]); 38 | 39 | const clearTerminal = useCallback(async () => { 40 | clearOutput("Running Python 3.12.7"); 41 | setError(null); 42 | }, [clearOutput, setError]); 43 | 44 | const commandHandlers: CommandHandlers = useMemo( 45 | () => ({ 46 | "pip install": async (input: string) => await pipInstall(input), 47 | "echo ": async (input: string) => 48 | await setOutput(input.split(" ").slice(1).join(" ")), 49 | pwd: getCwd, 50 | cwd: getCwd, 51 | clear: clearTerminal, 52 | cls: clearTerminal 53 | }), 54 | [pipInstall, setOutput, getCwd, clearTerminal] 55 | ); 56 | 57 | const handleReplSubmit = useCallback( 58 | async (e: React.FormEvent) => { 59 | e.preventDefault(); 60 | const formData = new FormData(e.currentTarget); 61 | const replInput = formData.get("terminalCode") as string; 62 | 63 | if (!replInput.trim()) return; 64 | 65 | setOutput(replInput); 66 | 67 | const handler = Object.entries(commandHandlers).find(([key]) => 68 | replInput.startsWith(key) 69 | ); 70 | 71 | if (handler) { 72 | await handler[1](replInput); 73 | } else { 74 | await runCode(replInput); 75 | } 76 | 77 | setTerminalCode(""); 78 | }, 79 | [runCode, setOutput, commandHandlers] 80 | ); 81 | 82 | if (isPyodideLoading) { 83 | return ( 84 |
85 | 86 |
87 | ); 88 | } 89 | 90 | return ( 91 |
95 | {(error || output) && ( 96 |
99 | {error || output} 100 |
101 | )} 102 |
103 | >>> 104 | ) => 108 | setTerminalCode(e.target.value) 109 | } 110 | className="w-full bg-transparent text-gray-300 outline-none" 111 | autoComplete="off" 112 | placeholder="..." 113 | /> 114 |
115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/store/useStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import type { PyodideInterface } from "@/types"; 3 | 4 | type Direction = "horizontal" | "vertical"; 5 | 6 | interface State { 7 | code: string; 8 | output: string; 9 | error: string | null; 10 | direction: Direction; 11 | pyodide: PyodideInterface | null; 12 | isPyodideLoading: boolean; 13 | isCodeExecuting: boolean; 14 | isLibLoading: boolean; 15 | } 16 | 17 | interface Actions { 18 | setCode: (code: string) => void; 19 | setOutput: (newOutput: string) => void; 20 | clearOutput: (defaultValue?: string) => void; 21 | setError: (error: string | null) => void; 22 | setDirection: (direction: Direction) => void; 23 | initializePyodide: () => Promise; 24 | pipInstall: (packageName: string) => Promise; 25 | runCode: (code: string) => Promise; 26 | } 27 | 28 | const initialState: State = { 29 | code: `import sys\n\nprint("Python", sys.version)\n\n# https://github.com/vwh/python-playground`, 30 | output: "Running Python 3.12.7", 31 | error: null, 32 | direction: "vertical", 33 | pyodide: null, 34 | isPyodideLoading: true, 35 | isCodeExecuting: false, 36 | isLibLoading: false 37 | }; 38 | 39 | export const useStore = create((set, get) => ({ 40 | ...initialState, 41 | 42 | setCode: (code) => set({ code }), 43 | setOutput: (newOutput) => 44 | set((state) => ({ output: `${state.output}\n${newOutput}` })), 45 | clearOutput: (defaultValue = "") => set({ output: defaultValue }), 46 | setError: (error) => set({ error }), 47 | setDirection: (direction) => set({ direction }), 48 | 49 | initializePyodide: async () => { 50 | if (!window.loadPyodide) { 51 | set({ error: "Pyodide script not loaded.", isPyodideLoading: false }); 52 | return; 53 | } 54 | 55 | try { 56 | const pyodideInstance = await window.loadPyodide(); 57 | await pyodideInstance.loadPackage("micropip"); 58 | const micropip = await pyodideInstance.pyimport("micropip"); 59 | window.micropip = micropip; 60 | set({ pyodide: pyodideInstance, isPyodideLoading: false }); 61 | } catch (error) { 62 | console.error("Failed to load Pyodide:", error); 63 | set({ 64 | error: "Failed to load Pyodide. Please refresh the page and try again.", 65 | isPyodideLoading: false 66 | }); 67 | } 68 | }, 69 | 70 | pipInstall: async (packageName: string) => { 71 | const { setOutput, setError } = get(); 72 | const lib = packageName.replace("pip install ", "").trim(); 73 | 74 | if (!window.micropip || !lib) return; 75 | set({ isLibLoading: true }); 76 | 77 | try { 78 | await window.micropip.install(lib, true); 79 | setOutput(`pip install ${lib} successfully installed`); 80 | setError(null); 81 | } catch (e) { 82 | setError(`Failed to install ${lib}: ${(e as Error).message}`); 83 | } finally { 84 | set({ isLibLoading: false }); 85 | } 86 | }, 87 | 88 | runCode: async (code: string) => { 89 | const { pyodide, setError, setOutput } = get(); 90 | if (!pyodide) return; 91 | 92 | set({ isCodeExecuting: true }); 93 | try { 94 | setError(null); 95 | const printOutput: string[] = []; 96 | 97 | pyodide.globals.set("print", (...args: any[]) => { 98 | const result = args.join(" "); 99 | printOutput.push(result); 100 | setOutput(printOutput.join("\n")); 101 | }); 102 | 103 | await pyodide.runPythonAsync(code); 104 | } catch (error) { 105 | console.error(error); 106 | setError( 107 | error instanceof Error ? error.message : "An unknown error occurred" 108 | ); 109 | } finally { 110 | set({ isCodeExecuting: false }); 111 | } 112 | } 113 | })); 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | tgram 5 | 6 | 7 | # Python Playground 8 | 9 |

10 | Browser-based Python playground built using WebAssembly (Pyodide) and ReactJS. Run and test Python code directly in your browser with an interactive and user-friendly interface. 11 |

12 | 13 | 27 | 28 |
29 | 30 | 31 | Preview 32 | 33 | 34 |
35 | 36 | ## Features 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | 77 | 82 | 83 |

Python in Browser

WebAssembly Powered

Library Support
46 | • Run Python code instantly
47 | • No server-side processing
48 | • Immediate feedback and results 49 |
51 | • Pyodide integration
52 | • Near-native performance
53 | • Seamless Python execution 54 |
56 | • Install packages via micropip
57 | • Access to wide range of libraries
58 | • Expand functionality on-the-fly 59 |

Terminal Commands

Intuitive Interface

Fast and Responsive
68 | • Support for pwd, clear, etc.
69 | • Familiar command-line experience
70 | • Enhanced debugging capabilities 71 |
73 | • Clean and modern UI design
74 | • User-friendly code editor
75 | • Intuitive layout and controls 76 |
78 | • Quick code execution
79 | • Responsive editor interface
80 | • Smooth user experience 81 |
84 | 85 | ## Contributing 86 | 87 | Contributions are welcome! Feel free to open a pull request with your improvements or fixes. 88 | 89 | ## License 90 | 91 | Under the MIT License. See [License](https://github.com/vwh/python-playground/blob/main/LICENSE) for more information. 92 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Python Playground 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 28 | 34 | 40 | 46 | 47 | 51 | 55 | 56 | 60 | 64 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 78 | 82 | 83 | 97 | 98 | 102 | 103 | 104 | 105 |
106 | 107 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["main"] 17 | pull_request: 18 | branches: ["main"] 19 | schedule: 20 | - cron: "29 20 * * 2" 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | # required for all workflows 34 | security-events: write 35 | 36 | # required to fetch internal or private CodeQL packs 37 | packages: read 38 | 39 | # only required for workflows in private repositories 40 | actions: read 41 | contents: read 42 | 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | include: 47 | - language: javascript-typescript 48 | build-mode: none 49 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 50 | # Use `c-cpp` to analyze code written in C, C++ or both 51 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 52 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 53 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 54 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 55 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 56 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v4 60 | 61 | # Initializes the CodeQL tools for scanning. 62 | - name: Initialize CodeQL 63 | uses: github/codeql-action/init@v3 64 | with: 65 | languages: ${{ matrix.language }} 66 | build-mode: ${{ matrix.build-mode }} 67 | # If you wish to specify custom queries, you can do so here or in a config file. 68 | # By default, queries listed here will override any specified in a config file. 69 | # Prefix the list here with "+" to use these queries and those in the config file. 70 | 71 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 72 | # queries: security-extended,security-and-quality 73 | 74 | # If the analyze step fails for one of the languages you are analyzing with 75 | # "We were unable to automatically build your code", modify the matrix above 76 | # to set the build mode to "manual" for that language. Then modify this step 77 | # to build your code. 78 | # ℹ️ Command-line programs to run using the OS shell. 79 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 80 | - if: matrix.build-mode == 'manual' 81 | shell: bash 82 | run: | 83 | echo 'If you are using a "manual" build mode for one or more of the' \ 84 | 'languages you are analyzing, replace this with the commands to build' \ 85 | 'your code, for example:' 86 | echo ' make bootstrap' 87 | echo ' make release' 88 | exit 1 89 | 90 | - name: Perform CodeQL Analysis 91 | uses: github/codeql-action/analyze@v3 92 | with: 93 | category: "/language:${{matrix.language}}" 94 | -------------------------------------------------------------------------------- /src/components/settings.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useStore } from "@/store/useStore"; 3 | 4 | import { Input } from "./ui/input"; 5 | import { Button } from "./ui/button"; 6 | import { 7 | Drawer, 8 | DrawerClose, 9 | DrawerContent, 10 | DrawerDescription, 11 | DrawerFooter, 12 | DrawerHeader, 13 | DrawerTitle, 14 | DrawerTrigger 15 | } from "./ui/drawer"; 16 | 17 | import { 18 | DownloadIcon, 19 | UploadIcon, 20 | SettingsIcon, 21 | LoaderIcon 22 | } from "lucide-react"; 23 | 24 | interface SettingsSectionProps { 25 | title: string; 26 | children: React.ReactNode; 27 | } 28 | 29 | function SettingsSection({ title, children }: SettingsSectionProps) { 30 | return ( 31 |
32 |

{title}

33 |
{children}
34 |
35 | ); 36 | } 37 | 38 | export default function Settings() { 39 | const { code, pipInstall, isLibLoading } = useStore(); 40 | 41 | const handleDownloadCode = useCallback(() => { 42 | const blob = new Blob([code], { type: "text/plain" }); 43 | const url = URL.createObjectURL(blob); 44 | const link = document.createElement("a"); 45 | link.download = "code.py"; 46 | link.href = url; 47 | link.click(); 48 | URL.revokeObjectURL(url); 49 | }, [code]); 50 | 51 | const handlePipInstall = useCallback( 52 | async (e: React.FormEvent) => { 53 | e.preventDefault(); 54 | const packageName = new FormData(e.currentTarget).get("lib") as string; 55 | await pipInstall(packageName); 56 | }, 57 | [pipInstall] 58 | ); 59 | 60 | const handleCodeShare = useCallback(() => { 61 | const urlParams = new URLSearchParams(window.location.search); 62 | urlParams.set("v", btoa(code)); 63 | const newUrl = `${window.location.origin}${window.location.pathname}?${urlParams.toString()}${window.location.hash}`; 64 | window.history.replaceState({}, document.title, newUrl); 65 | navigator.clipboard.writeText(newUrl); 66 | }, [code]); 67 | 68 | return ( 69 | 70 | 71 | 79 | 80 | 81 |
82 | 83 | Settings 84 | 85 | Personalize your site experience here. 86 | 87 | 88 |
89 | 90 |
91 | 98 | 110 |
111 |
112 | 113 | 122 | 131 | 132 |
133 | 134 | 135 | 138 | 139 | 140 |
141 |
142 |
143 | ); 144 | } 145 | --------------------------------------------------------------------------------