├── src ├── vite-env.d.ts ├── tailwind-material-colors.d.ts ├── templates │ ├── main.c │ ├── main.cpp │ └── with-conui │ │ ├── main.c │ │ ├── main.cpp │ │ └── conui.h ├── App.css ├── assets │ ├── fonts │ │ ├── Google Sans Text │ │ │ ├── GoogleSansText-Bold.ttf │ │ │ ├── GoogleSansText-Italic.ttf │ │ │ ├── GoogleSansText-Medium.ttf │ │ │ ├── GoogleSansText-Regular.ttf │ │ │ ├── GoogleSansText-BoldItalic.ttf │ │ │ └── GoogleSansText-MediumItalic.ttf │ │ └── Google Sans Display │ │ │ ├── GoogleSansDisplay-Bold.ttf │ │ │ ├── GoogleSansDisplay-Italic.ttf │ │ │ ├── GoogleSansDisplay-Medium.ttf │ │ │ ├── GoogleSansDisplay-Regular.ttf │ │ │ ├── GoogleSansDisplay-BoldItalic.ttf │ │ │ └── GoogleSansDisplay-MediumItalic.ttf │ └── react.svg ├── main.tsx ├── components │ ├── Link.tsx │ ├── FileIcon.tsx │ ├── RadioButton.tsx │ ├── Button.tsx │ ├── Dialog.tsx │ ├── PUBCodeIcon.tsx │ ├── About.tsx │ ├── Folder.tsx │ ├── TitleBar.tsx │ ├── Splash.tsx │ ├── File.tsx │ └── NewProject.tsx ├── utils │ ├── materialTheme.ts │ └── i18n.ts ├── index.css └── App.tsx ├── src-tauri ├── build.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 32x32.png │ ├── icon.icns │ ├── 128x128.png │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── .gitignore ├── Cargo.toml ├── src │ └── main.rs ├── pub-code.iss ├── tauri.conf.json └── pub-code-mingw64.iss ├── app-icon.png ├── .vscode └── extensions.json ├── postcss.config.cjs ├── tsconfig.node.json ├── tailwind.config.cjs ├── .gitignore ├── index.html ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── package.json ├── public ├── vite.svg └── tauri.svg ├── README.md └── pnpm-lock.yaml /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/app-icon.png -------------------------------------------------------------------------------- /src/tailwind-material-colors.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tailwind-material-colors/lib/updateTheme.esm"; 2 | -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src/templates/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void main() 5 | { 6 | printf("Hello, world!"); 7 | getch(); 8 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .logo.vite:hover { 2 | filter: drop-shadow(0 0 2em #747bff); 3 | } 4 | 5 | .logo.react:hover { 6 | filter: drop-shadow(0 0 2em #61dafb); 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Text/GoogleSansText-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Text/GoogleSansText-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Text/GoogleSansText-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Text/GoogleSansText-Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Text/GoogleSansText-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Text/GoogleSansText-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Text/GoogleSansText-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Text/GoogleSansText-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Display/GoogleSansDisplay-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Display/GoogleSansDisplay-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Text/GoogleSansText-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Text/GoogleSansText-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Display/GoogleSansDisplay-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Display/GoogleSansDisplay-Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Display/GoogleSansDisplay-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Display/GoogleSansDisplay-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Display/GoogleSansDisplay-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Display/GoogleSansDisplay-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Text/GoogleSansText-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Text/GoogleSansText-MediumItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Display/GoogleSansDisplay-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Display/GoogleSansDisplay-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Google Sans Display/GoogleSansDisplay-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romikusumabakti/pub-code/HEAD/src/assets/fonts/Google Sans Display/GoogleSansDisplay-MediumItalic.ttf -------------------------------------------------------------------------------- /src/templates/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace std; 5 | 6 | int main() 7 | { 8 | cout << "Hello, world!"; 9 | getch(); 10 | return 0; 11 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/with-conui/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "libs/conui.h" 4 | 5 | void main() 6 | { 7 | set_cursor_pos(53, 14); 8 | set_text_color(YELLOW); 9 | set_bg_color(DARK_BLUE); 10 | printf("Hello, world!"); 11 | getch(); 12 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/templates/with-conui/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "libs/conui.h" 4 | 5 | using namespace std; 6 | 7 | int main() 8 | { 9 | set_cursor_pos(53, 14); 10 | set_text_color(YELLOW); 11 | set_bg_color(DARK_BLUE); 12 | cout << "Hello, world!"; 13 | getch(); 14 | return 0; 15 | } -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const { withMaterialColors } = require("tailwind-material-colors"); 3 | module.exports = withMaterialColors( 4 | { 5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | darkMode: "class", 11 | }, 12 | { primary: "#448aff" } 13 | ); 14 | -------------------------------------------------------------------------------- /.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 | 26 | analytics.ts 27 | mingw64 -------------------------------------------------------------------------------- /src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { shell } from "@tauri-apps/api"; 2 | import { AnchorHTMLAttributes } from "react"; 3 | 4 | interface LinkProps extends AnchorHTMLAttributes { 5 | to?: string; 6 | children?: any; 7 | } 8 | 9 | function Link({ to, children }: LinkProps) { 10 | return shell.open(to || children)}>{children}; 11 | } 12 | 13 | export default Link; 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/FileIcon.tsx: -------------------------------------------------------------------------------- 1 | import { VscFile, VscFileBinary, VscFileCode } from "react-icons/vsc"; 2 | import PUBCodeIcon from "./PUBCodeIcon"; 3 | 4 | function FileIcon({ name }: { name?: string }) { 5 | return ( 6 | 7 | {name?.endsWith(".c") ? ( 8 | 9 | ) : name?.endsWith(".exe") ? ( 10 | 11 | ) : name === "pub-code.json" ? ( 12 | 13 | ) : ( 14 | 15 | )} 16 | 17 | ); 18 | } 19 | 20 | export default FileIcon; 21 | -------------------------------------------------------------------------------- /src/components/RadioButton.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes } from "react"; 2 | 3 | interface RadioButtonProps extends InputHTMLAttributes { 4 | children: any; 5 | disabled?: boolean; 6 | } 7 | 8 | function RadioButton({ 9 | children, 10 | disabled = false, 11 | ...rest 12 | }: RadioButtonProps) { 13 | return ( 14 | 15 | 19 | 20 | ); 21 | } 22 | 23 | export default RadioButton; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from "react"; 2 | 3 | type Variant = "filled" | "tonal"; 4 | 5 | interface ButtonProps extends ButtonHTMLAttributes { 6 | variant?: Variant; 7 | className?: string; 8 | } 9 | 10 | function Button({ variant = "filled", className, ...props }: ButtonProps) { 11 | const styles: Record = { 12 | filled: "interactive-bg-primary", 13 | tonal: "interactive-bg-secondary-container", 14 | }; 15 | 16 | return ( 17 | 80 | 81 | ); 82 | } 83 | 84 | export default About; 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PUB Code icon](./src-tauri/icons/128x128.png) 2 | 3 | # PUB Code 4 | 5 | [![PUB Code version](https://img.shields.io/github/v/release/romikusumabakti/pub-code.svg?include_prereleases)](https://github.com/romikusumabakti/pub-code/releases) 6 | [![PUB Code downloads](https://img.shields.io/github/downloads/romikusumabakti/pub-code/total.svg)](https://github.com/romikusumabakti/pub-code/releases) 7 | 8 | PUB Code (pronounced `/pé u bé koʊd/`) is a fast, simple, and open source C/C++ IDE for Windows. 9 | 10 | ![PUB Code screenshot](https://user-images.githubusercontent.com/41172548/211024120-83f420a0-3c77-4f83-aa70-872840d5ec69.jpg#gh-dark-mode-only)![PUB Code screenshot](https://user-images.githubusercontent.com/41172548/211024136-2f0363d6-f30f-4cc1-92df-2482280ba25b.jpg#gh-light-mode-only) 11 | 12 | > [PUB](https://www.pubpasim.org/) (pronounced `/pé u bé/`, _Pemberdayaan Umat Berkelanjutan_) is a scholarship program that provides opportunities for high school graduates to continue their studies (at [Universitas Nasional PASIM](https://pasim.ac.id/), Bandung, Indonesia), learn to code, and become full-stack developers. 13 | 14 | ## ✨ Features 15 | 16 | - Syntax highlighting ([Monaco Editor](https://microsoft.github.io/monaco-editor/)) 17 | - Debugging (with GDB) 18 | - Dark theme support 19 | - Color scheme ([Material Design 3](https://m3.material.io/)) 20 | 21 | ## 🌐 Languages 22 | 23 | - English 24 | - Bahasa Indonesia 25 | 26 | ## 💾 Installation 27 | 28 | To install PUB Code on your computer, follow these steps: 29 | 30 | 1. Make sure you have [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) installed on your computer. 31 | 2. Download the latest version of PUB Code from the [releases page](https://github.com/romikusumabakti/pub-code/releases). 32 | 3. Run the installer and follow the prompts to install PUB Code on your computer. 33 | 4. Once the installation is complete, you can launch PUB Code from the Start menu or by double-clicking the PUB Code shortcut on your desktop. 34 | 35 | > On Windows 10 (April 2018 release or later) and Windows 11, the WebView2 runtime is distributed as part of the operating system. 36 | 37 | ## 📦 Third party libraries 38 | 39 | - [Tauri](https://tauri.app/) (desktop app framework) 40 | - [Monaco Editor](https://microsoft.github.io/monaco-editor/) (code editor) 41 | - [WinLibs](https://winlibs.com/) (GCC and MinGW-w64 build for Windows) 42 | - [React](https://reactjs.org/) (UI library) 43 | - [Tailwind CSS](https://tailwindcss.com/) (UI styling) 44 | - [Vite](https://vitejs.dev/) (UI build tool) 45 | 46 | ## 📜 License 47 | 48 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 49 | -------------------------------------------------------------------------------- /src-tauri/pub-code.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define AppName "PUB Code" 5 | #define AppVersion "0.7.0-beta" 6 | #define AppPublisher "PUB Education Division" 7 | #define AppURL "https://next.pubpasim.org/code" 8 | #define AppExeName "PUB Code.exe" 9 | #define AppAssocName "Source Code File" 10 | #define AppAssocExt ".c" 11 | #define AppAssocKey StringChange(AppAssocName, " ", "") + AppAssocExt 12 | 13 | [Setup] 14 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 15 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 16 | AppId={{35B5925D-372A-4AB7-A982-91DD1C3BEF20} 17 | AppName={#AppName} 18 | AppVersion={#AppVersion} 19 | ;AppVerName={#AppName} {#AppVersion} 20 | AppPublisher={#AppPublisher} 21 | AppPublisherURL={#AppURL} 22 | AppSupportURL={#AppURL} 23 | AppUpdatesURL={#AppURL} 24 | DefaultDirName={autopf}\{#AppName} 25 | ChangesAssociations=yes 26 | DisableProgramGroupPage=yes 27 | LicenseFile=..\LICENSE 28 | ; Remove the following line to run in administrative install mode (install for all users.) 29 | PrivilegesRequired=lowest 30 | OutputDir=target\release\bundle 31 | OutputBaseFilename=PUBCodeUserSetup-x64-{#AppVersion} 32 | SetupIconFile=icons\icon.ico 33 | Compression=lzma 34 | SolidCompression=yes 35 | WizardStyle=modern 36 | 37 | [Languages] 38 | Name: "english"; MessagesFile: "compiler:Default.isl" 39 | 40 | [Tasks] 41 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 42 | 43 | [Files] 44 | Source: "target\release\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion 45 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 46 | 47 | [Registry] 48 | Root: HKA; Subkey: "Software\Classes\{#AppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#AppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue 49 | Root: HKA; Subkey: "Software\Classes\{#AppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#AppAssocName}"; Flags: uninsdeletekey 50 | Root: HKA; Subkey: "Software\Classes\{#AppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#AppExeName},0" 51 | Root: HKA; Subkey: "Software\Classes\{#AppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}"" ""%1""" 52 | Root: HKA; Subkey: "Software\Classes\Applications\{#AppExeName}\SupportedTypes"; ValueType: string; ValueName: ".p"; ValueData: "" 53 | 54 | [Icons] 55 | Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}" 56 | Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon 57 | 58 | [Run] 59 | Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 60 | 61 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "pnpm dev", 4 | "beforeBuildCommand": "pnpm build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist" 7 | }, 8 | "package": { 9 | "productName": "PUB Code", 10 | "version": "0.7.0-beta" 11 | }, 12 | "tauri": { 13 | "allowlist": { 14 | "all": true, 15 | "fs": { 16 | "scope": ["*"] 17 | }, 18 | "shell": { 19 | "scope": [ 20 | { 21 | "name": "build_c", 22 | "cmd": "C:/mingw64/bin/gcc", 23 | "args": [ 24 | "-g", 25 | { "validator": "\\S+" }, 26 | "-o", 27 | { "validator": "\\S+" } 28 | ] 29 | }, 30 | { 31 | "name": "build_cpp", 32 | "cmd": "C:/mingw64/bin/g++", 33 | "args": [ 34 | "-g", 35 | { "validator": "\\S+" }, 36 | "-o", 37 | { "validator": "\\S+" } 38 | ] 39 | }, 40 | { 41 | "name": "run", 42 | "cmd": "C:/mingw64/bin/gdb", 43 | "args": [ 44 | { "validator": "\\S+" }, 45 | "-batch", 46 | "-ex", 47 | "set new-console on", 48 | "-ex", 49 | { "validator": "\\S+" }, 50 | "-ex", 51 | "run" 52 | ] 53 | } 54 | ] 55 | } 56 | }, 57 | "bundle": { 58 | "active": false, 59 | "category": "DeveloperTool", 60 | "copyright": "", 61 | "deb": { 62 | "depends": [] 63 | }, 64 | "externalBin": [], 65 | "icon": [ 66 | "icons/32x32.png", 67 | "icons/128x128.png", 68 | "icons/128x128@2x.png", 69 | "icons/icon.icns", 70 | "icons/icon.ico" 71 | ], 72 | "identifier": "org.pubpasim.code", 73 | "longDescription": "", 74 | "macOS": { 75 | "entitlements": null, 76 | "exceptionDomain": "", 77 | "frameworks": [], 78 | "providerShortName": null, 79 | "signingIdentity": null 80 | }, 81 | "resources": [], 82 | "shortDescription": "", 83 | "targets": "all", 84 | "windows": { 85 | "certificateThumbprint": null, 86 | "digestAlgorithm": "sha256", 87 | "timestampUrl": "" 88 | } 89 | }, 90 | "security": { 91 | "csp": null 92 | }, 93 | "updater": { 94 | "active": false 95 | }, 96 | "windows": [ 97 | { 98 | "fullscreen": false, 99 | "resizable": true, 100 | "title": "PUB Code", 101 | "minWidth": 800, 102 | "minHeight": 600, 103 | "decorations": false 104 | } 105 | ] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src-tauri/pub-code-mingw64.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define AppName "PUB Code" 5 | #define AppVersion "0.7.0-beta" 6 | #define AppPublisher "PUB Education Division" 7 | #define AppURL "https://next.pubpasim.org/code" 8 | #define AppExeName "PUB Code.exe" 9 | #define AppAssocName "Source Code File" 10 | #define AppAssocExt ".c" 11 | #define AppAssocKey StringChange(AppAssocName, " ", "") + AppAssocExt 12 | 13 | [Setup] 14 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 15 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 16 | AppId={{35B5925D-372A-4AB7-A982-91DD1C3BEF20} 17 | AppName={#AppName} 18 | AppVersion={#AppVersion} 19 | ;AppVerName={#AppName} {#AppVersion} 20 | AppPublisher={#AppPublisher} 21 | AppPublisherURL={#AppURL} 22 | AppSupportURL={#AppURL} 23 | AppUpdatesURL={#AppURL} 24 | DefaultDirName={autopf}\{#AppName} 25 | ChangesAssociations=yes 26 | DisableProgramGroupPage=yes 27 | LicenseFile=..\LICENSE 28 | ; Remove the following line to run in administrative install mode (install for all users.) 29 | PrivilegesRequired=lowest 30 | OutputDir=target\release\bundle 31 | OutputBaseFilename=PUBCodeMinGWUserSetup-x64-{#AppVersion} 32 | SetupIconFile=icons\icon.ico 33 | Compression=lzma 34 | SolidCompression=yes 35 | WizardStyle=modern 36 | 37 | [Languages] 38 | Name: "english"; MessagesFile: "compiler:Default.isl" 39 | 40 | [Tasks] 41 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 42 | 43 | [Files] 44 | Source: "target\release\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion 45 | Source: "mingw64\*"; DestDir: "C:\mingw64\"; Flags: ignoreversion recursesubdirs createallsubdirs 46 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 47 | 48 | [Registry] 49 | Root: HKA; Subkey: "Software\Classes\{#AppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#AppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue 50 | Root: HKA; Subkey: "Software\Classes\{#AppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#AppAssocName}"; Flags: uninsdeletekey 51 | Root: HKA; Subkey: "Software\Classes\{#AppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#AppExeName},0" 52 | Root: HKA; Subkey: "Software\Classes\{#AppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}"" ""%1""" 53 | Root: HKA; Subkey: "Software\Classes\Applications\{#AppExeName}\SupportedTypes"; ValueType: string; ValueName: ".p"; ValueData: "" 54 | 55 | [Icons] 56 | Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}" 57 | Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon 58 | 59 | [Run] 60 | Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 61 | 62 | -------------------------------------------------------------------------------- /src/components/Folder.tsx: -------------------------------------------------------------------------------- 1 | import { FileEntry, readDir } from "@tauri-apps/api/fs"; 2 | import { ButtonHTMLAttributes, useEffect, useState } from "react"; 3 | import { VscChevronDown, VscChevronRight, VscNewFile } from "react-icons/vsc"; 4 | import File from "./File"; 5 | import { watchImmediate } from "tauri-plugin-fs-watch-api"; 6 | 7 | interface EntryProps extends ButtonHTMLAttributes { 8 | path: string; 9 | name?: string; 10 | level?: number; 11 | open?: boolean; 12 | } 13 | 14 | function Folder({ path, name, level = 0, open = false }: EntryProps) { 15 | const [children, setChildren] = useState([]); 16 | const [isOpen, setIsOpen] = useState(open); 17 | const [isNewFile, setIsNewFile] = useState(false); 18 | 19 | async function fetchChildren() { 20 | if (path) { 21 | const entries = await readDir(path); 22 | setChildren(entries); 23 | } 24 | } 25 | 26 | useEffect(() => { 27 | if (path) { 28 | watchImmediate(path, { recursive: false }, (e) => { 29 | if (e.operation !== 16) { 30 | fetchChildren(); 31 | } 32 | }); 33 | } 34 | if (open) { 35 | fetchChildren(); 36 | } 37 | }, []); 38 | 39 | return ( 40 | <> 41 |
{ 48 | if (isOpen) { 49 | setIsOpen(false); 50 | } else { 51 | await fetchChildren(); 52 | setIsOpen(true); 53 | } 54 | }} 55 | > 56 | {isOpen ? : } 57 | 62 | {name} 63 | 64 | 73 |
74 | {isOpen && [ 75 | children! 76 | .filter((child) => child.children) 77 | .map((child) => ( 78 | 84 | )), 85 | isNewFile && ( 86 | setIsNewFile(false)} 91 | /> 92 | ), 93 | children! 94 | .filter((child) => !child.children) 95 | .map((child) => ( 96 | 103 | )), 104 | ]} 105 | 106 | ); 107 | } 108 | 109 | export default Folder; 110 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import { appWindow } from "@tauri-apps/api/window"; 2 | import { useContext, useEffect, useState } from "react"; 3 | import { MdDarkMode, MdLightMode } from "react-icons/md"; 4 | import { 5 | VscChromeClose, 6 | VscChromeMaximize, 7 | VscChromeMinimize, 8 | VscChromeRestore, 9 | } from "react-icons/vsc"; 10 | import { FileContext, ICommand, ThemeContext } from "../App"; 11 | import { useTranslation } from "react-i18next"; 12 | import PUBCodeIcon from "./PUBCodeIcon"; 13 | 14 | interface TitleBarProps { 15 | menu: ICommand[]; 16 | } 17 | 18 | function TitleBar({ menu }: TitleBarProps) { 19 | const { i18n } = useTranslation(); 20 | const { theme, setTheme, darkTheme, darkThemeMediaQueryList } = 21 | useContext(ThemeContext) || {}; 22 | const [isMaximized, setIsMaximized] = useState(); 23 | const { currentFolderPath, openedFiles, currentFileIndex } = 24 | useContext(FileContext) || {}; 25 | 26 | async function updateIsMaximized() { 27 | const isMaximized = await appWindow.isMaximized(); 28 | if (isMaximized) { 29 | setIsMaximized(true); 30 | } else { 31 | setIsMaximized(false); 32 | } 33 | } 34 | 35 | useEffect(() => { 36 | appWindow.onResized(updateIsMaximized); 37 | updateIsMaximized(); 38 | }, []); 39 | 40 | return ( 41 |
45 | 46 | 50 | 51 | 52 | 53 | {menu.map((button: ICommand) => ( 54 | 62 | ))} 63 | 64 | 65 | 69 | {[ 70 | openedFiles && 71 | typeof currentFileIndex === "number" && 72 | openedFiles.length > 0 && 73 | openedFiles[currentFileIndex]?.name, 74 | currentFolderPath?.substring(currentFolderPath.lastIndexOf("\\") + 1), 75 | "PUB Code", 76 | ] 77 | .filter((s) => s) 78 | .join(" - ")} 79 | 80 | 84 | 92 | 106 | 109 | {isMaximized ? ( 110 | 113 | ) : ( 114 | 117 | )} 118 | 121 | 122 |
123 | ); 124 | } 125 | 126 | export default TitleBar; 127 | -------------------------------------------------------------------------------- /src/components/Splash.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useContext } from "react"; 2 | import { MdDarkMode, MdLanguage, MdLightMode, MdPalette } from "react-icons/md"; 3 | import { ColorContext, CommandContext, Theme, ThemeContext } from "../App"; 4 | import { useTranslation } from "react-i18next"; 5 | import Button from "./Button"; 6 | import Dialog from "./Dialog"; 7 | import { VscClose } from "react-icons/vsc"; 8 | import { AboutFooter, AboutHeader } from "./About"; 9 | import { sendStats } from "../utils/analytics"; 10 | import { colors } from "../utils/materialTheme"; 11 | 12 | interface SplashProps { 13 | isOpen: boolean; 14 | setOpen: Dispatch>; 15 | } 16 | 17 | function Splash({ isOpen, setOpen }: SplashProps) { 18 | const { t, i18n } = useTranslation(); 19 | const { color, setColor } = useContext(ColorContext) || {}; 20 | const { theme, setTheme, darkTheme } = useContext(ThemeContext) || {}; 21 | const command = useContext(CommandContext); 22 | 23 | return ( 24 | setOpen(false)} 28 | > 29 | 30 | 31 |
32 | 60 | 83 | 102 |
103 | 104 | 107 | 114 | 120 | 121 | 127 |
128 | ); 129 | } 130 | 131 | export default Splash; 132 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Google Sans Display"; 7 | src: url("./assets/fonts/Google Sans Display/GoogleSansDisplay-Regular.ttf"); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: "Google Sans Display"; 14 | src: url("./assets/fonts/Google Sans Display/GoogleSansDisplay-Italic.ttf"); 15 | font-weight: normal; 16 | font-style: italic; 17 | } 18 | 19 | @font-face { 20 | font-family: "Google Sans Display"; 21 | src: url("./assets/fonts/Google Sans Display/GoogleSansDisplay-Medium.ttf"); 22 | font-weight: 500; 23 | font-style: normal; 24 | } 25 | 26 | @font-face { 27 | font-family: "Google Sans Display"; 28 | src: url("./assets/fonts/Google Sans Display/GoogleSansDisplay-MediumItalic.ttf"); 29 | font-weight: 500; 30 | font-style: italic; 31 | } 32 | 33 | @font-face { 34 | font-family: "Google Sans Display"; 35 | src: url("./assets/fonts/Google Sans Display/GoogleSansDisplay-Bold.ttf"); 36 | font-weight: bold; 37 | font-style: normal; 38 | } 39 | 40 | @font-face { 41 | font-family: "Google Sans Display"; 42 | src: url("./assets/fonts/Google Sans Display/GoogleSansDisplay-BoldItalic.ttf"); 43 | font-weight: bold; 44 | font-style: italic; 45 | } 46 | 47 | @font-face { 48 | font-family: "Google Sans Text"; 49 | src: url("./assets/fonts/Google Sans Text/GoogleSansText-Regular.ttf"); 50 | font-weight: normal; 51 | font-style: normal; 52 | } 53 | 54 | @font-face { 55 | font-family: "Google Sans Text"; 56 | src: url("./assets/fonts/Google Sans Text/GoogleSansText-Italic.ttf"); 57 | font-weight: normal; 58 | font-style: italic; 59 | } 60 | 61 | @font-face { 62 | font-family: "Google Sans Text"; 63 | src: url("./assets/fonts/Google Sans Text/GoogleSansText-Medium.ttf"); 64 | font-weight: 500; 65 | font-style: normal; 66 | } 67 | 68 | @font-face { 69 | font-family: "Google Sans Text"; 70 | src: url("./assets/fonts/Google Sans Text/GoogleSansText-MediumItalic.ttf"); 71 | font-weight: 500; 72 | font-style: italic; 73 | } 74 | 75 | @font-face { 76 | font-family: "Google Sans Text"; 77 | src: url("./assets/fonts/Google Sans Text/GoogleSansText-Bold.ttf"); 78 | font-weight: bold; 79 | font-style: normal; 80 | } 81 | 82 | @font-face { 83 | font-family: "Google Sans Text"; 84 | src: url("./assets/fonts/Google Sans Text/GoogleSansText-BoldItalic.ttf"); 85 | font-weight: bold; 86 | font-style: italic; 87 | } 88 | 89 | :root { 90 | font-family: "Google Sans Text", sans-serif; 91 | @apply text-[13px] select-none; 92 | } 93 | 94 | ::-webkit-scrollbar { 95 | @apply w-2 h-2; 96 | } 97 | 98 | ::-webkit-scrollbar-track { 99 | @apply bg-surface3 rounded-full; 100 | } 101 | 102 | ::-webkit-scrollbar-thumb { 103 | @apply bg-surface-variant rounded-full; 104 | } 105 | 106 | body { 107 | margin: 0; 108 | } 109 | 110 | .display { 111 | font-family: "Google Sans Display", sans-serif; 112 | } 113 | 114 | header { 115 | @apply h-10 flex items-center justify-between shrink-0 uppercase text-[11px] px-4; 116 | } 117 | 118 | button, 119 | .button { 120 | @apply flex items-center bg-on-surface bg-opacity-0 cursor-auto; 121 | } 122 | 123 | button:hover, 124 | .button:hover { 125 | @apply bg-opacity-hover; 126 | } 127 | 128 | /* button:focus { 129 | @apply bg-opacity-focus; 130 | } */ 131 | 132 | button:disabled, 133 | .button:disabled { 134 | @apply text-on-surface text-opacity-disabled bg-opacity-0; 135 | } 136 | 137 | button.active, 138 | .button.active { 139 | @apply bg-surface-variant; 140 | } 141 | 142 | a { 143 | @apply text-primary cursor-pointer hover:underline; 144 | } 145 | 146 | input[type="text"], 147 | select { 148 | @apply grow h-8 px-2 rounded border border-outline outline-primary bg-transparent; 149 | } 150 | 151 | option { 152 | @apply bg-surface1; 153 | } 154 | 155 | .monaco-editor .margin, 156 | .monaco-editor-background { 157 | @apply !bg-surface; 158 | } 159 | 160 | .menu { 161 | @apply bg-surface1 rounded; 162 | } 163 | 164 | .menu > button { 165 | @apply px-3 h-12 w-full gap-3; 166 | } 167 | 168 | .menu > button:first-child { 169 | @apply rounded-t; 170 | } 171 | 172 | .menu > button:last-child { 173 | @apply rounded-b; 174 | } 175 | 176 | .menu > button > svg { 177 | @apply text-primary; 178 | } 179 | 180 | .text-opacity-disabled { 181 | @apply opacity-[.38]; 182 | } 183 | 184 | .bg-opacity-hover { 185 | @apply bg-opacity-[.08]; 186 | } 187 | 188 | .bg-surface1, 189 | .bg-surface3, 190 | .bg-surface5 { 191 | @apply bg-surface bg-gradient-to-t; 192 | } 193 | 194 | .bg-surface1 { 195 | @apply from-primary/[.05] to-primary/[.05]; 196 | } 197 | 198 | .bg-surface3 { 199 | @apply from-primary/[.11] to-primary/[.11]; 200 | } 201 | 202 | .bg-surface5 { 203 | @apply from-primary/[.14] to-primary/[.14]; 204 | } 205 | -------------------------------------------------------------------------------- /src/components/File.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | exists, 3 | readTextFile, 4 | removeFile, 5 | renameFile, 6 | writeTextFile, 7 | } from "@tauri-apps/api/fs"; 8 | import { basename, extname, join } from "@tauri-apps/api/path"; 9 | import { ButtonHTMLAttributes, useContext, useEffect, useState } from "react"; 10 | import { FileContext } from "../App"; 11 | import FileIcon from "./FileIcon"; 12 | import { BiRename } from "react-icons/bi"; 13 | import { VscTrash } from "react-icons/vsc"; 14 | import { useTranslation } from "react-i18next"; 15 | 16 | interface EntryProps extends ButtonHTMLAttributes { 17 | path?: string; 18 | name?: string; 19 | level?: number; 20 | folderPath: string; 21 | onCreate?: () => any; 22 | } 23 | 24 | function File({ path, name, level = 0, folderPath, onCreate }: EntryProps) { 25 | const { t } = useTranslation(); 26 | 27 | const [newName, setNewName] = useState(name); 28 | const [isRenaming, setIsRenaming] = useState(false); 29 | const [menuPosition, setMenuPosition] = useState(); 30 | 31 | const { openedFiles, setOpenedFiles, currentFileIndex, setCurrentFileIndex } = 32 | useContext(FileContext) || {}; 33 | 34 | useEffect(() => { 35 | if (!name) { 36 | setIsRenaming(true); 37 | } 38 | }, [name]); 39 | 40 | async function open(path: string) { 41 | setOpenedFiles!([ 42 | ...openedFiles!, 43 | { 44 | path, 45 | name: await basename(path), 46 | language: await extname(path), 47 | value: await readTextFile(path), 48 | }, 49 | ]); 50 | setCurrentFileIndex!(openedFiles!.length); 51 | } 52 | 53 | return ( 54 | <> 55 |
{ 64 | const fileIndex = openedFiles!.findIndex( 65 | (file) => file.path === path! 66 | ); 67 | if (fileIndex !== -1) { 68 | setCurrentFileIndex!(fileIndex); 69 | } else if (path) { 70 | open(path); 71 | } 72 | }} 73 | onContextMenu={(e) => { 74 | e.preventDefault(); 75 | setMenuPosition({ x: e.pageX, y: e.pageY }); 76 | }} 77 | > 78 | 79 | {isRenaming ? ( 80 | e.stopPropagation()} 86 | onChange={(e) => setNewName(e.target.value)} 87 | onKeyDown={async (e) => { 88 | if (e.key === "Enter") { 89 | e.currentTarget.blur(); 90 | } 91 | }} 92 | onBlur={async (e) => { 93 | if (path) { 94 | const newFilePath = await join( 95 | folderPath, 96 | newName || e.target.value 97 | ); 98 | if (onCreate) { 99 | if (e.target.value) { 100 | if (!(await exists(newFilePath))) { 101 | await writeTextFile(newFilePath, ""); 102 | open(newFilePath); 103 | } else { 104 | alert(t("explorer.nameError")); 105 | } 106 | } 107 | onCreate(); 108 | } else { 109 | if (newName && newName !== name) { 110 | if (!(await exists(newFilePath))) { 111 | renameFile(path, newFilePath); 112 | setOpenedFiles!( 113 | openedFiles!.map((file) => { 114 | if (file.path === path) { 115 | return { 116 | ...file, 117 | path: newFilePath, 118 | name: newName, 119 | }; 120 | } else { 121 | return file; 122 | } 123 | }) 124 | ); 125 | } else { 126 | alert(t("explorer.nameError")); 127 | } 128 | } else { 129 | setNewName(name); 130 | } 131 | setIsRenaming(false); 132 | } 133 | } 134 | }} 135 | /> 136 | ) : ( 137 | 142 | {name} 143 | 144 | )} 145 |
146 | {menuPosition && ( 147 |
setMenuPosition(null)} 150 | > 151 |
160 | 164 | 180 |
181 |
182 | )} 183 | 184 | ); 185 | } 186 | 187 | export default File; 188 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | export const resources = { 2 | en: { 3 | translation: { 4 | description: "C/C++ Integrated Development Environment", 5 | pubEduDiv: "PUB Education Division", 6 | pubNext: "PUB Next", 7 | create: "Create", 8 | cancel: "Cancel", 9 | name: "Name", 10 | aLibrary: "{{library}} library", 11 | language: "Language", 12 | loading: "Loading", 13 | about: { 14 | version: "Version", 15 | projectFounder: "Project founder", 16 | license: "License", 17 | partOfPubNext: "Part of the <0>{{pubNext}} project.", 18 | }, 19 | newProject: { 20 | title: "New project", 21 | location: "Location", 22 | language: "Language", 23 | mainProgram: "Main program", 24 | compilerPath: "Compiler path", 25 | initialize: "Initialize", 26 | nameError: 27 | "There is already a folder with the same name in this location.", 28 | }, 29 | command: { 30 | new: "New", 31 | open: "Open", 32 | save: "Save", 33 | build: "Build", 34 | run: "Run", 35 | stop: "Stop", 36 | runFile: "Run file", 37 | log: "Log", 38 | about: "About", 39 | zoomIn: "Zoom in", 40 | zoomOut: "Zoom out", 41 | resetZoom: "Reset zoom", 42 | }, 43 | color: { 44 | title: "Color", 45 | red: "Red", 46 | redAccent: "Red accent", 47 | pink: "Pink", 48 | pinkAccent: "Pink accent", 49 | purple: "Purple", 50 | purpleAccent: "Purple accent", 51 | deepPurple: "Deep purple", 52 | deepPurpleAccent: "Deep purple accent", 53 | indigo: "Indigo", 54 | indigoAccent: "Indigo accent", 55 | blue: "Blue", 56 | blueAccent: "Blue accent", 57 | lightBlue: "Light blue", 58 | lightBlueAccent: "Light blue accent", 59 | cyan: "Cyan", 60 | cyanAccent: "Cyan accent", 61 | teal: "Teal", 62 | tealAccent: "Teal accent", 63 | green: "Green", 64 | greenAccent: "Green accent", 65 | lightGreen: "Light green", 66 | lightGreenAccent: "Light green accent", 67 | lime: "Lime", 68 | limeAccent: "Lime accent", 69 | yellow: "Yellow", 70 | yellowAccent: "Yellow accent", 71 | amber: "Amber", 72 | amberAccent: "Amber accent", 73 | orange: "Orange", 74 | orangeAccent: "Orange accent", 75 | deepOrange: "Deep orange", 76 | deepOrangeAccent: "Deep orange accent", 77 | brown: "Brown", 78 | grey: "Grey", 79 | blueGrey: "Blue grey", 80 | }, 81 | theme: { 82 | title: "Theme", 83 | system: "System", 84 | light: "Light", 85 | dark: "Dark", 86 | }, 87 | explorer: { 88 | title: "Explorer", 89 | rename: "Rename", 90 | delete: "Delete", 91 | deleteConfirm: "Are you sure you want to delete this file?", 92 | nameError: 93 | "There is already a file with the same name in this location.", 94 | }, 95 | status: { 96 | reading: "Reading", 97 | formatting: "Formatting", 98 | saving: "Saving", 99 | compiling: "Compiling", 100 | running: "Running", 101 | line: "Line", 102 | column: "Column", 103 | }, 104 | log: { 105 | openMainProgramFile: "Open main program file", 106 | fileSaved: "File saved", 107 | compilation: { 108 | finished: "Compilation finished", 109 | failed: "Compilation failed", 110 | aborted: "Compilation aborted", 111 | }, 112 | runningFinished: "Running finished", 113 | }, 114 | }, 115 | }, 116 | id: { 117 | translation: { 118 | description: "Integrated Development Environment C/C++", 119 | pubEduDiv: "Divisi Pendidikan PUB", 120 | pubNext: "PUB Next", 121 | create: "Buat", 122 | cancel: "Batal", 123 | name: "Nama", 124 | aLibrary: "library {{library}}", 125 | language: "Bahasa", 126 | loading: "Memuat", 127 | about: { 128 | version: "Versi", 129 | projectFounder: "Pendiri project", 130 | license: "Lisensi", 131 | partOfPubNext: "Bagian dari project <0>{{pubNext}}.", 132 | }, 133 | newProject: { 134 | title: "Project baru", 135 | location: "Lokasi", 136 | language: "Bahasa", 137 | mainProgram: "Program utama", 138 | compilerPath: "Jalur compiler", 139 | initialize: "Inisialisasi", 140 | nameError: "Sudah ada folder dengan nama yang sama di lokasi ini.", 141 | }, 142 | command: { 143 | new: "Baru", 144 | open: "Buka", 145 | save: "Simpan", 146 | build: "Build", 147 | run: "Jalankan", 148 | stop: "Hentikan", 149 | runFile: "Jalankan file", 150 | log: "Log", 151 | about: "Tentang", 152 | zoomIn: "Perbesar", 153 | zoomOut: "Perkecil", 154 | resetZoom: "Reset zoom", 155 | }, 156 | color: { 157 | title: "Warna", 158 | red: "Merah", 159 | redAccent: "Aksen merah", 160 | pink: "Merah muda", 161 | pinkAccent: "Aksen merah muda", 162 | purple: "Ungu", 163 | purpleAccent: "Aksen ungu", 164 | deepPurple: "Ungu tua", 165 | deepPurpleAccent: "Aksen ungu tua", 166 | indigo: "Nila", 167 | indigoAccent: "Aksen nila", 168 | blue: "Biru", 169 | blueAccent: "Aksen biru", 170 | lightBlue: "Biru muda", 171 | lightBlueAccent: "Aksen biru muda", 172 | cyan: "Sian", 173 | cyanAccent: "Aksen sian", 174 | teal: "Teal", 175 | tealAccent: "Aksen teal", 176 | green: "Hijau", 177 | greenAccent: "Aksen hijau", 178 | lightGreen: "Hijau muda", 179 | lightGreenAccent: "Aksen hijau muda", 180 | lime: "Hijau pupus", 181 | limeAccent: "Aksen hijau pupus", 182 | yellow: "Kuning", 183 | yellowAccent: "Aksen kuning", 184 | amber: "Kuning lulur", 185 | amberAccent: "Aksen kuning lulur", 186 | orange: "Oranye", 187 | orangeAccent: "Aksen oranye", 188 | deepOrange: "Oranye tua", 189 | deepOrangeAccent: "Aksen oranye tua", 190 | brown: "Coklat", 191 | grey: "Abu-abu", 192 | blueGrey: "Abu-abu biru", 193 | }, 194 | theme: { 195 | title: "Tema", 196 | system: "Sistem", 197 | light: "Terang", 198 | dark: "Gelap", 199 | }, 200 | explorer: { 201 | title: "Penjelajah", 202 | rename: "Ubah nama", 203 | delete: "Hapus", 204 | deleteConfirm: "Apakah Anda yakin ingin menghapus file ini?", 205 | nameError: "Sudah ada file dengan nama yang sama di lokasi ini.", 206 | }, 207 | status: { 208 | reading: "Membaca", 209 | formatting: "Memformat", 210 | saving: "Menyimpan", 211 | compiling: "Mengompilasi", 212 | running: "Menjalankan", 213 | line: "Baris", 214 | column: "Kolom", 215 | }, 216 | log: { 217 | openMainProgramFile: "Membuka file program utama", 218 | fileSaved: "File disimpan", 219 | compilation: { 220 | finished: "Kompilasi selesai", 221 | failed: "Kompilasi gagal", 222 | aborted: "Kompilasi dibatalkan", 223 | }, 224 | runningFinished: "Penjalanan selesai", 225 | }, 226 | }, 227 | }, 228 | }; 229 | -------------------------------------------------------------------------------- /src/components/NewProject.tsx: -------------------------------------------------------------------------------- 1 | import { open } from "@tauri-apps/api/dialog"; 2 | import { createDir, exists, writeTextFile } from "@tauri-apps/api/fs"; 3 | import { documentDir, join } from "@tauri-apps/api/path"; 4 | import { Dispatch, SetStateAction, useEffect, useState } from "react"; 5 | import { useTranslation } from "react-i18next"; 6 | import { VscFolderOpened } from "react-icons/vsc"; 7 | import { sendStats } from "../utils/analytics"; 8 | import Button from "./Button"; 9 | import Dialog from "./Dialog"; 10 | import RadioButton from "./RadioButton"; 11 | import helloWorldC from "../templates/main.c?raw"; 12 | import helloWorldCpp from "../templates/main.cpp?raw"; 13 | import helloWorldWithConuiC from "../templates/with-conui/main.c?raw"; 14 | import helloWorldWithConuiCpp from "../templates/with-conui/main.cpp?raw"; 15 | import conuiH from "../templates/with-conui/conui.h?raw"; 16 | 17 | type LanguageId = "c" | "cpp" | "carbon"; 18 | 19 | interface Language { 20 | id: LanguageId; 21 | name: string; 22 | disabled?: boolean; 23 | } 24 | 25 | const languages: Language[] = [ 26 | { 27 | id: "c", 28 | name: "C", 29 | }, 30 | { 31 | id: "cpp", 32 | name: "C++", 33 | }, 34 | { 35 | id: "carbon", 36 | name: "Carbon", 37 | disabled: true, 38 | }, 39 | ]; 40 | 41 | const helloWorld = { 42 | c: helloWorldC, 43 | cpp: helloWorldCpp, 44 | carbon: "", 45 | }; 46 | 47 | const helloWorldWithConui = { 48 | c: helloWorldWithConuiC, 49 | cpp: helloWorldWithConuiCpp, 50 | carbon: "", 51 | }; 52 | 53 | interface NewProjectProps { 54 | isOpen: boolean; 55 | setIsOpen: Dispatch>; 56 | onCreate: (location: string) => void; 57 | } 58 | 59 | interface Configuration { 60 | name: string; 61 | version: string; 62 | language: LanguageId; 63 | mainProgram: string; 64 | } 65 | 66 | function NewProject({ isOpen, setIsOpen, onCreate }: NewProjectProps) { 67 | const { t } = useTranslation(); 68 | 69 | const [configuration, setConfiguration] = useState({ 70 | name: "project-1", 71 | version: "0.1.0", 72 | language: "c", 73 | mainProgram: "./main", 74 | }); 75 | const [location, setLocation] = useState(""); 76 | const [initHelloWorld, setInitHelloWorld] = useState(false); 77 | const [initConui, setInitConui] = useState(false); 78 | 79 | useEffect(() => { 80 | if (isOpen) { 81 | documentDir().then(async (documentDir) => { 82 | const pubCodePath = await join(documentDir, "pub-code"); 83 | setLocation(pubCodePath); 84 | let projectNumber = 1; 85 | while (true) { 86 | const projectName = `project-${projectNumber}`; 87 | const projectPath = await join(pubCodePath, projectName); 88 | if (await exists(projectPath)) { 89 | projectNumber++; 90 | } else { 91 | setConfiguration({ 92 | ...configuration, 93 | name: projectName, 94 | }); 95 | break; 96 | } 97 | } 98 | }); 99 | } 100 | }, [isOpen]); 101 | 102 | return ( 103 | setIsOpen(false)} 107 | > 108 |

{t("newProject.title")}

109 |
{ 112 | e.preventDefault(); 113 | const projectPath = await join(location, configuration.name); 114 | if (!(await exists(projectPath))) { 115 | await createDir(await join(projectPath, "build"), { 116 | recursive: true, 117 | }); 118 | await writeTextFile( 119 | await join(projectPath, "pub-code.json"), 120 | JSON.stringify(configuration, null, 2) 121 | ); 122 | const mainProgramPath = await join( 123 | projectPath, 124 | `${configuration.mainProgram}.${configuration.language}` 125 | ); 126 | if (initConui) { 127 | await writeTextFile( 128 | mainProgramPath, 129 | initHelloWorld 130 | ? helloWorldWithConui[configuration.language] 131 | : "" 132 | ); 133 | await createDir(await join(projectPath, "libs"), { 134 | recursive: true, 135 | }); 136 | await writeTextFile( 137 | await join(projectPath, "libs", "conui.h"), 138 | conuiH 139 | ); 140 | } else { 141 | await writeTextFile( 142 | mainProgramPath, 143 | initHelloWorld ? helloWorld[configuration.language] : "" 144 | ); 145 | } 146 | onCreate(projectPath); 147 | setIsOpen(false); 148 | sendStats("create_project", configuration); 149 | } else { 150 | alert(t("newProject.nameError")); 151 | } 152 | }} 153 | > 154 |
155 |
156 |
157 | {t("name")} 158 | 162 | setConfiguration({ ...configuration, name: e.target.value }) 163 | } 164 | required 165 | autoFocus 166 | onFocus={(e) => e.target.select()} 167 | /> 168 |
169 |
170 | {t("newProject.location")} 171 | 172 | setLocation(e.target.value)} 176 | required 177 | /> 178 | 194 | 195 |
196 |
197 | {t("newProject.language")} 198 | {languages.map((language) => ( 199 | 204 | setConfiguration({ 205 | ...configuration, 206 | language: language.id, 207 | }) 208 | } 209 | disabled={language.disabled} 210 | > 211 | {language.name} 212 | 213 | ))} 214 |
215 |
216 | {t("newProject.mainProgram")} 217 | 221 | setConfiguration({ 222 | ...configuration, 223 | mainProgram: e.target.value, 224 | }) 225 | } 226 | required 227 | /> 228 |
229 |
230 |
231 | 239 | 248 |
249 |
250 |
251 | 252 | 255 |
256 |
257 |
258 | ); 259 | } 260 | 261 | export default NewProject; 262 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | Dispatch, 4 | Fragment, 5 | SetStateAction, 6 | useEffect, 7 | useRef, 8 | useState, 9 | } from "react"; 10 | import "./App.css"; 11 | 12 | import { open } from "@tauri-apps/api/dialog"; 13 | import { 14 | createDir, 15 | exists, 16 | FileEntry, 17 | readDir, 18 | readTextFile, 19 | writeTextFile, 20 | } from "@tauri-apps/api/fs"; 21 | // import { register, registerAll } from "@tauri-apps/api/globalShortcut"; 22 | import { basename, dirname, extname, join } from "@tauri-apps/api/path"; 23 | import { Child, Command } from "@tauri-apps/api/shell"; 24 | import { invoke } from "@tauri-apps/api/tauri"; 25 | 26 | import i18next from "i18next"; 27 | import { useTranslation, initReactI18next } from "react-i18next"; 28 | import LanguageDetector from "i18next-browser-languagedetector"; 29 | import { resources } from "./utils/i18n"; 30 | 31 | import { updateTheme as updateScheme } from "tailwind-material-colors/lib/updateTheme.esm"; 32 | import { colors } from "./utils/materialTheme"; 33 | 34 | import { VscClose, VscDebugStop, VscPlay } from "react-icons/vsc"; 35 | 36 | import { Allotment, LayoutPriority } from "allotment"; 37 | import "allotment/dist/style.css"; 38 | 39 | import TitleBar from "./components/TitleBar"; 40 | import Folder from "./components/Folder"; 41 | import FileIcon from "./components/FileIcon"; 42 | import Splash from "./components/Splash"; 43 | import NewProject from "./components/NewProject"; 44 | import About from "./components/About"; 45 | 46 | import Editor, { loader } from "@monaco-editor/react"; 47 | import * as monaco from "monaco-editor"; 48 | import { sendStats } from "./utils/analytics"; 49 | import { init, format } from "wastyle"; 50 | import astyleBinaryUrl from "wastyle/dist/astyle.wasm?url"; 51 | import { watchImmediate } from "tauri-plugin-fs-watch-api"; 52 | 53 | let stopWatching = async () => {}; 54 | 55 | init(astyleBinaryUrl); 56 | 57 | let zoomLevel = parseInt(localStorage.getItem("zoom")!) || 0; 58 | 59 | function updateZoom(): number { 60 | localStorage.setItem("zoom", zoomLevel.toString()); 61 | const zoomFactor = Math.pow(1.2, zoomLevel); 62 | invoke("zoom", { factor: zoomFactor }); 63 | return zoomFactor; 64 | } 65 | 66 | document.oncontextmenu = (e) => e.preventDefault(); 67 | 68 | i18next 69 | .use(LanguageDetector) 70 | .use(initReactI18next) 71 | .init({ 72 | resources, 73 | fallbackLng: "en", 74 | 75 | interpolation: { 76 | escapeValue: false, 77 | }, 78 | }); 79 | 80 | export type Theme = "system" | "light" | "dark"; 81 | 82 | interface IColorContext { 83 | color: string; 84 | setColor: Dispatch>; 85 | } 86 | 87 | interface IThemeContext { 88 | theme: Theme; 89 | setTheme: Dispatch>; 90 | darkTheme?: boolean; 91 | darkThemeMediaQueryList?: MediaQueryList; 92 | } 93 | 94 | export interface ICommand { 95 | title: string; 96 | shortcut?: string; 97 | action: (...args: any[]) => any; 98 | disabled?: boolean; 99 | } 100 | 101 | interface File { 102 | path: string; 103 | name: string; 104 | language: string; 105 | value: string; 106 | } 107 | 108 | interface IFileContext { 109 | currentFolderPath: string; 110 | openedFiles: File[]; 111 | setOpenedFiles: Dispatch>; 112 | currentFileIndex: number; 113 | setCurrentFileIndex: Dispatch>; 114 | } 115 | 116 | export const ColorContext = createContext(null); 117 | export const ThemeContext = createContext({ 118 | theme: "system", 119 | setTheme: () => null, 120 | }); 121 | export const CommandContext = createContext>({}); 122 | export const FileContext = createContext(null); 123 | 124 | loader.config({ monaco }); 125 | 126 | let editingPath: string; 127 | 128 | const storedOpenedFiles = await Promise.all( 129 | JSON.parse(localStorage.getItem("openedFiles") || "[]").map( 130 | async (path: string) => { 131 | const value = await readTextFile(path); 132 | const language = await extname(path); 133 | monaco.editor.createModel( 134 | value, 135 | language, 136 | monaco.Uri.parse(`file:///${path}`) 137 | ); 138 | return { 139 | path, 140 | name: await basename(path), 141 | language, 142 | value, 143 | }; 144 | } 145 | ) 146 | ); 147 | 148 | function App() { 149 | const { i18n, t } = useTranslation(); 150 | 151 | const [color, setColor] = useState( 152 | localStorage.getItem("color") || "blueAccent" 153 | ); 154 | const [theme, setTheme] = useState( 155 | (localStorage.getItem("theme") as Theme) || "system" 156 | ); 157 | const [darkTheme, setDarkTheme] = useState(); 158 | 159 | const editorRef = useRef(null); 160 | const [position, setPosition] = useState(); 161 | 162 | const logRef = useRef(null); 163 | 164 | const [isSplashOpen, setIsSplashOpen] = useState(true); 165 | const [isAboutOpen, setIsAboutOpen] = useState(false); 166 | const [isLogOpen, setIsLogOpen] = useState(true); 167 | 168 | const [currentFolderPath, setCurrentFolderPath] = useState( 169 | localStorage.getItem("currentFolderPath")! 170 | ); 171 | const [currentProjectConfig, setCurrentProjectConfig] = useState< 172 | Record 173 | >({}); 174 | const [entries, setEntries] = useState([]); 175 | const [openedFiles, setOpenedFiles] = useState(storedOpenedFiles); 176 | const [currentFileIndex, setCurrentFileIndex] = useState( 177 | parseInt(localStorage.getItem("currentFileIndex")!) || 0 178 | ); 179 | 180 | const [isNewProjectOpen, setIsNewProjectOpen] = useState(false); 181 | const [isOpenFolderOpen, setIsOpenFolderOpen] = useState(false); 182 | const [isSaving, setIsSaving] = useState(false); 183 | const [isBuilding, setIsBuilding] = useState(false); 184 | const [debuggingChild, setDebuggingChild] = useState(); 185 | 186 | const [status, setStatus] = useState(); 187 | 188 | function log(data: string) { 189 | logRef.current!.value += data + "\n"; 190 | logRef.current?.scrollTo(0, logRef.current.scrollHeight); 191 | } 192 | 193 | async function openFolder(path: string, start = openedFiles.length === 0) { 194 | localStorage.setItem("currentFolderPath", path); 195 | const entries = await readDir(path); 196 | setEntries([ 197 | { 198 | path, 199 | name: path.substring(path.lastIndexOf("\\") + 1), 200 | children: entries, 201 | }, 202 | ]); 203 | const configFileName = "pub-code.json"; 204 | const pubCodeJson = entries.find((entry) => entry.name === configFileName); 205 | logRef.current!.value = ""; 206 | if (pubCodeJson) { 207 | log(`${t("status.reading")} ${configFileName}...`); 208 | const contents = await readTextFile(pubCodeJson.path); 209 | const projectConfig = JSON.parse(contents); 210 | if (start) { 211 | if (projectConfig.mainProgram) { 212 | openMainProgram(path, projectConfig); 213 | } else { 214 | setOpenedFiles([]); 215 | } 216 | } 217 | setCurrentProjectConfig(projectConfig); 218 | } else if (start) { 219 | setOpenedFiles([]); 220 | } 221 | } 222 | 223 | async function openMainProgram( 224 | projectPath: string, 225 | projectConfig: Record 226 | ) { 227 | const mainProgramFileName = `${projectConfig.mainProgram}.${projectConfig.language}`; 228 | const path = await join(projectPath, mainProgramFileName); 229 | log(`${t("log.openMainProgramFile")} (${mainProgramFileName})...`); 230 | setOpenedFiles([ 231 | { 232 | path, 233 | name: await basename(path), 234 | language: await extname(path), 235 | value: await readTextFile(path), 236 | }, 237 | ]); 238 | } 239 | 240 | async function save() { 241 | if (editorRef.current) { 242 | editingPath = editorRef.current 243 | .getModel() 244 | ?.uri.path.substring(1) as string; 245 | await writeTextFile(editingPath, editorRef.current.getValue()); 246 | } 247 | } 248 | 249 | async function build(sourcePath: string, callback?: Function) { 250 | setIsBuilding(true); 251 | log(`${t("status.compiling")}...`); 252 | setStatus(`${t("status.compiling")}...`); 253 | const buildFolderPath = await join(currentFolderPath, "build"); 254 | if (!(await exists(buildFolderPath))) { 255 | await createDir(buildFolderPath, { 256 | recursive: true, 257 | }); 258 | } 259 | const language = await extname(sourcePath); 260 | const program = await basename( 261 | sourcePath, 262 | `.${currentProjectConfig.language || language}` 263 | ); 264 | const binaryPath = await join(currentFolderPath, "build", `${program}.exe`); 265 | const command = new Command(`build_${language}`, [ 266 | "-g", 267 | sourcePath, 268 | "-o", 269 | binaryPath, 270 | ]); 271 | let error = false; 272 | command.stderr.on("data", (data) => { 273 | if (data.includes(": warning: ")) { 274 | setIsLogOpen(true); 275 | } else if (data.includes(": error: ")) { 276 | error = true; 277 | setIsLogOpen(true); 278 | } 279 | log(data); 280 | }); 281 | command.on("close", async (data) => { 282 | setDebuggingChild(undefined); 283 | setStatus(""); 284 | if (data.code === 0) { 285 | sendStats("build"); 286 | log(`${t("log.compilation.finished")} (${binaryPath}).`); 287 | if (typeof callback === "function") { 288 | await callback(binaryPath); 289 | } 290 | } else { 291 | if (error) { 292 | log(`${t("log.compilation.failed")}.`); 293 | } else { 294 | log(`${t("log.compilation.aborted")}.`); 295 | } 296 | } 297 | setIsBuilding(false); 298 | }); 299 | setDebuggingChild(await command.spawn()); 300 | } 301 | 302 | async function run(binaryPath: string, cwd: string) { 303 | if (binaryPath) { 304 | log(`${t("status.running")}...`); 305 | setStatus(`${t("status.running")}...`); 306 | const command = new Command("run", [ 307 | binaryPath, 308 | "-batch", 309 | "-ex", 310 | '"set new-console on"', 311 | "-ex", 312 | `set cwd ${cwd}`, 313 | "-ex", 314 | "run", 315 | ]); 316 | command.stdout.on("data", (data) => log(data)); 317 | command.on("close", () => { 318 | setDebuggingChild(undefined); 319 | log(`${t("log.runningFinished")}.`); 320 | setStatus(""); 321 | }); 322 | setDebuggingChild(await command.spawn()); 323 | } 324 | } 325 | 326 | const command: Record = { 327 | new: { 328 | title: t("command.new"), 329 | shortcut: "CommandOrControl+N", 330 | action: () => setIsNewProjectOpen(true), 331 | disabled: isNewProjectOpen, 332 | }, 333 | open: { 334 | title: t("command.open"), 335 | shortcut: "CommandOrControl+O", 336 | action: async () => { 337 | setIsOpenFolderOpen(true); 338 | const selected = await open({ 339 | directory: true, 340 | }); 341 | setIsOpenFolderOpen(false); 342 | if (typeof selected === "string") { 343 | await openFolder(selected, true); 344 | setIsSplashOpen(false); 345 | setCurrentFolderPath(selected); 346 | } 347 | }, 348 | disabled: isOpenFolderOpen, 349 | }, 350 | save: { 351 | title: t("command.save"), 352 | shortcut: "CommandOrControl+S", 353 | action: async (callback: Function) => { 354 | if (editorRef.current && openedFiles.length > 0) { 355 | logRef.current!.value = ""; 356 | try { 357 | await editorRef.current 358 | .getAction("editor.action.formatDocument") 359 | .run(); 360 | } catch (e) { 361 | console.error(e); 362 | } 363 | setIsSaving(true); 364 | log(`${t("status.saving")}...`); 365 | setStatus(`${t("status.saving")}...`); 366 | await save(); 367 | log(`${t("log.fileSaved")} (${openedFiles[currentFileIndex].path}).`); 368 | setIsSaving(false); 369 | setStatus(""); 370 | if (callback) { 371 | callback(); 372 | } 373 | } 374 | }, 375 | disabled: openedFiles.length === 0 || isSaving, 376 | }, 377 | build: { 378 | title: t("command.build"), 379 | shortcut: "CommandOrControl+B", 380 | action: async (callback?: Function) => { 381 | if (openedFiles.length > 0 && currentProjectConfig) { 382 | const sourcePath = await join( 383 | currentFolderPath, 384 | `${currentProjectConfig.mainProgram}.${currentProjectConfig.language}` 385 | ); 386 | build(sourcePath, callback); 387 | } 388 | }, 389 | disabled: !currentProjectConfig.mainProgram || isBuilding, 390 | }, 391 | run: { 392 | title: t("command.run"), 393 | shortcut: "CommandOrControl+R", 394 | action: () => 395 | command.build.action((binaryPath: string) => 396 | run(binaryPath, currentFolderPath) 397 | ), 398 | disabled: 399 | !currentProjectConfig.mainProgram || 400 | isBuilding || 401 | debuggingChild !== undefined, 402 | }, 403 | runFile: { 404 | title: t("command.runFile"), 405 | shortcut: "CommandOrControl+Shift+R", 406 | action: async () => { 407 | if (openedFiles.length > 0) { 408 | const fileDir = await dirname(openedFiles[currentFileIndex].path); 409 | build(openedFiles[currentFileIndex].path, (binaryPath: string) => 410 | run(binaryPath, fileDir) 411 | ); 412 | } 413 | }, 414 | disabled: !( 415 | currentFileIndex < openedFiles.length && 416 | (openedFiles[currentFileIndex].name.endsWith(".c") || 417 | openedFiles[currentFileIndex].name.endsWith(".cpp")) 418 | ), 419 | }, 420 | stop: { 421 | title: t("command.stop"), 422 | shortcut: "CommandOrControl+K", 423 | action: () => debuggingChild?.kill(), 424 | }, 425 | log: { 426 | title: t("command.log"), 427 | shortcut: "CommandOrControl+`", 428 | action: () => setIsLogOpen((prevLogOpen) => !prevLogOpen), 429 | }, 430 | about: { 431 | title: t("command.about"), 432 | action: () => { 433 | sendStats("view_about"); 434 | setIsAboutOpen(true); 435 | }, 436 | disabled: isAboutOpen, 437 | }, 438 | zoomIn: { 439 | title: t("command.zoomIn"), 440 | shortcut: "CommandOrControl+=", 441 | action: () => { 442 | if (zoomLevel < 3) { 443 | zoomLevel++; 444 | const zoomFactor = updateZoom(); 445 | log( 446 | `${t("command.zoomIn")} ${zoomFactor.toLocaleString("id-ID", { 447 | style: "percent", 448 | })}` 449 | ); 450 | } 451 | }, 452 | }, 453 | zoomOut: { 454 | title: t("command.zoomOut"), 455 | shortcut: "CommandOrControl+-", 456 | action: () => { 457 | if (zoomLevel > -3) { 458 | zoomLevel--; 459 | const zoomFactor = updateZoom(); 460 | log( 461 | `${t("command.zoomOut")} ${zoomFactor.toLocaleString("id-ID", { 462 | style: "percent", 463 | })}` 464 | ); 465 | } 466 | }, 467 | }, 468 | resetZoom: { 469 | title: t("command.resetZoom"), 470 | shortcut: "CommandOrControl+0", 471 | action: () => { 472 | zoomLevel = 0; 473 | const zoomFactor = updateZoom(); 474 | log( 475 | `${t("command.resetZoom")} ${zoomFactor.toLocaleString("id-ID", { 476 | style: "percent", 477 | })}` 478 | ); 479 | }, 480 | }, 481 | }; 482 | 483 | // function registerAllcommand() { 484 | // Object.values(command).forEach((command) => { 485 | // if (command.shortcut) { 486 | // register(command.shortcut, command.action); 487 | // } 488 | // }); 489 | // } 490 | 491 | useEffect(() => { 492 | sendStats("start", { language: i18n.language, theme, color }); 493 | monaco.languages.registerDocumentFormattingEditProvider(["c", "cpp"], { 494 | async provideDocumentFormattingEdits(model) { 495 | const [success, result] = format(model.getValue(), "pad-oper"); 496 | log(`${t("status.formatting")}...`); 497 | if (success) { 498 | return [ 499 | { 500 | range: model.getFullModelRange(), 501 | text: result, 502 | }, 503 | ]; 504 | } 505 | }, 506 | }); 507 | const zoomFactor = Math.pow(1.2, zoomLevel); 508 | invoke("zoom", { factor: zoomFactor }); 509 | // appWindow.onFocusChanged(({ payload: focused }) => { 510 | // if (focused) { 511 | // registerAllcommand(); 512 | // } else { 513 | // unregisterAll(); 514 | // } 515 | // }); 516 | const currentFolderPath = localStorage.getItem("currentFolderPath"); 517 | if (currentFolderPath) { 518 | openFolder(currentFolderPath); 519 | } 520 | }, []); 521 | 522 | useEffect(() => { 523 | updateScheme( 524 | { 525 | primary: colors[color], 526 | }, 527 | "class" 528 | ); 529 | localStorage.setItem("color", color); 530 | }, [color]); 531 | 532 | const darkThemeMediaQueryList = matchMedia("(prefers-color-scheme: dark)"); 533 | const resolvedTheme = 534 | theme === "system" 535 | ? darkThemeMediaQueryList.matches 536 | ? "dark" 537 | : "light" 538 | : theme; 539 | 540 | useEffect(() => { 541 | setDarkTheme(resolvedTheme === "dark"); 542 | if (theme === "system") { 543 | localStorage.removeItem("theme"); 544 | darkThemeMediaQueryList.onchange = () => 545 | setDarkTheme(darkThemeMediaQueryList.matches); 546 | return () => { 547 | darkThemeMediaQueryList.onchange = null; 548 | }; 549 | } else { 550 | localStorage.setItem("theme", theme); 551 | } 552 | }, [theme]); 553 | 554 | useEffect(() => { 555 | document.documentElement.className = resolvedTheme; 556 | document.documentElement.style.colorScheme = resolvedTheme; 557 | }, [darkTheme]); 558 | 559 | useEffect(() => { 560 | localStorage.setItem( 561 | "openedFiles", 562 | JSON.stringify(openedFiles.map((openedFile) => openedFile.path)) 563 | ); 564 | if (openedFiles.length > 0 && currentFileIndex >= openedFiles.length) { 565 | setCurrentFileIndex(openedFiles.length - 1); 566 | } 567 | }, [openedFiles]); 568 | 569 | useEffect(() => { 570 | localStorage.setItem("currentFileIndex", currentFileIndex.toString()); 571 | }, [currentFileIndex]); 572 | 573 | useEffect(() => { 574 | editingPath = ""; 575 | watchImmediate( 576 | openedFiles.map((openedFile) => openedFile.path), 577 | { recursive: false }, 578 | async (e) => { 579 | if (e.path) { 580 | if (e.operation === 4 || e.operation === 4) { 581 | setOpenedFiles( 582 | openedFiles.filter((openedFile) => openedFile.path !== e.path) 583 | ); 584 | } else if (e.operation === 16) { 585 | const value = await readTextFile(e.path); 586 | // setOpenedFiles( 587 | // openedFiles.map((openedFile) => { 588 | // if (openedFile.path === e.path) { 589 | // return { ...openedFile, value }; 590 | // } else { 591 | // return openedFile; 592 | // } 593 | // }) 594 | // ); 595 | if (e.path !== editingPath) { 596 | monaco.editor.getModels().forEach((model) => { 597 | if (e.path && model.uri.path.endsWith(e.path)) { 598 | model.setValue(value); 599 | } 600 | }); 601 | } 602 | } 603 | } 604 | } 605 | ).then((watch) => (stopWatching = watch)); 606 | // registerAllcommand(); 607 | return () => { 608 | stopWatching(); 609 | // unregisterAll(); 610 | }; 611 | }, [currentFileIndex, openedFiles]); 612 | 613 | useEffect(() => { 614 | onkeydown = (e) => { 615 | if ( 616 | e.ctrlKey && 617 | e.key !== "F5" && 618 | !( 619 | e.key.toLowerCase() === "c" || 620 | e.key.toLowerCase() === "v" || 621 | e.key.toLowerCase() === "x" 622 | ) 623 | ) { 624 | e.preventDefault(); 625 | for (let key in command) { 626 | if ( 627 | command[key].shortcut && 628 | e.key !== "Control" && 629 | e.key !== "Shift" 630 | ) { 631 | const buttons: string[] = []; 632 | if (e.ctrlKey) { 633 | buttons.push("CommandOrControl"); 634 | } 635 | if (e.shiftKey) { 636 | buttons.push("Shift"); 637 | } 638 | if (e.key) { 639 | buttons.push(e.key.toUpperCase()); 640 | } 641 | if (buttons.join("+") === command[key].shortcut) { 642 | if (!command[key].disabled) { 643 | command[key].action(); 644 | } 645 | break; 646 | } 647 | } 648 | } 649 | } 650 | }; 651 | return () => { 652 | onkeydown = null; 653 | }; 654 | }, [currentFileIndex, openedFiles, currentProjectConfig]); 655 | 656 | return ( 657 | 658 | 661 | 662 | 671 |
672 | 683 |
684 | { 688 | if (sizes[0]) { 689 | localStorage.setItem("sideBarWidth", sizes[0].toString()); 690 | } 691 | }} 692 | > 693 | 697 |
698 |
{t("explorer.title")}
699 |
700 | {entries.map((entry) => ( 701 | 707 | ))} 708 |
709 |
710 |
711 | 712 |
713 |
0 && "bg-surface1" 716 | }`} 717 | > 718 | 719 | {openedFiles.map((file, i) => ( 720 | 721 | {i === 0 && ( 722 | 728 | )} 729 | setCurrentFileIndex(i)} 740 | title={file.path} 741 | > 742 | 743 | 744 | {file.name} 745 | 746 | 758 | 759 | 766 | 767 | ))} 768 | 769 | 770 | {!command.runFile.disabled && 771 | (isBuilding || debuggingChild !== undefined ? ( 772 | 779 | ) : ( 780 | 791 | ))} 792 | 793 |
794 | {currentFileIndex < openedFiles.length && ( 795 | { 804 | editor.onDidChangeCursorPosition((e) => 805 | setPosition(e.position) 806 | ); 807 | editor.onDidChangeModelContent(async (e) => { 808 | if (!e.isFlush) { 809 | await save(); 810 | } 811 | }); 812 | editorRef.current = editor; 813 | }} 814 | /> 815 | )} 816 |
817 | 822 |
823 |
824 | Log 825 | 831 |
832 |