diff --git a/package.json b/package.json index a1a93ba..5d9f3e3 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,23 @@ "@hookform/resolvers": "^3.3.2", "@open-rpc/client-js": "^1.8.1", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tooltip": "^1.0.7", "@replit/codemirror-emacs": "^6.0.1", "@replit/codemirror-vim": "^6.1.0", "@tauri-apps/api": "^1.5.1", + "@types/ws": "^8.5.10", "axios": "^1.6.2", "axios-tauri-api-adapter": "^0.2.0", "class-variance-authority": "^0.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8592cb..cd14953 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,15 @@ dependencies: '@radix-ui/react-accordion': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-alert-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-context-menu': specifier: ^2.1.5 version: 2.1.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) @@ -59,6 +65,9 @@ dependencies: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-progress': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-select': specifier: ^2.0.0 version: 2.0.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) @@ -68,6 +77,9 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-tooltip': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) '@replit/codemirror-emacs': specifier: ^6.0.1 version: 6.0.1(@codemirror/autocomplete@6.11.0)(@codemirror/commands@6.3.0)(@codemirror/search@6.5.4)(@codemirror/state@6.3.1)(@codemirror/view@6.22.0) @@ -77,6 +89,9 @@ dependencies: '@tauri-apps/api': specifier: ^1.5.1 version: 1.5.1 + '@types/ws': + specifier: ^8.5.10 + version: 8.5.10 axios: specifier: ^1.6.2 version: 1.6.2 @@ -1186,6 +1201,32 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.37)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-dom': 18.2.15 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -1231,6 +1272,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-dom': 18.2.15 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: @@ -1729,6 +1798,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-progress@1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-dom': 18.2.15 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -1865,6 +1956,38 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.37)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.37 + '@types/react-dom': 18.2.15 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0): resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} peerDependencies: @@ -2281,6 +2404,12 @@ packages: resolution: {integrity: sha512-86XLCVEmWagiUEbr2AjSbeY4qHN9jMm3pgM3PuBYfLIbT0MpDSnA3GA/4W7KoH/C/eeK77kNaeIxZzjhKYIBgw==} dev: false + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + dependencies: + '@types/node': 20.9.1 + dev: false + /@vitejs/plugin-react@4.2.0(vite@4.5.0): resolution: {integrity: sha512-+MHTH/e6H12kRp5HUkzOGqPMksezRMmW+TNzlh/QXfI8rRf6l2Z2yH/v12no1UvTwhZgEDMuQ7g7rrfMseU6FQ==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6902822..9bd40d5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -672,6 +672,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "embed-resource" version = "2.4.0" @@ -1030,6 +1036,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1270,6 +1286,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "html5ever" version = "0.25.2" @@ -2218,6 +2243,7 @@ dependencies = [ "dunce", "fancy-regex", "futures-util", + "gethostname", "log", "once_cell", "rand 0.8.5", @@ -2232,7 +2258,9 @@ dependencies = [ "tempfile", "tokio", "tokio-tungstenite", + "toml 0.8.8", "uuid", + "which", ] [[package]] @@ -3783,6 +3811,19 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4ef9ec0..1d3192d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -38,6 +38,9 @@ fancy-regex = "0.12.0" tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] } tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } rusqlite = { version = "0.30.0", features = ["bundled"] } +gethostname = "0.4.3" +which = "5.0.0" +toml = "0.8.8" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 62b12b6..e01b699 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -4,13 +4,12 @@ fn gcc(inp: &str, oup: &str) { if PathBuf::from(oup).exists() { return; } - assert!(Command::new("c++") - .args([inp, "-std=c++17", "-o", oup]) - .spawn() - .unwrap() - .wait() - .unwrap() - .success()); + let mut cmd = Command::new("c++"); + cmd.args([inp, "-std=c++17", "-o", oup]); + if cfg!(windows) { + cmd.arg("-static"); + } + assert!(cmd.spawn().unwrap().wait().unwrap().success()); } fn main() { if cfg!(windows) { diff --git a/src-tauri/scripts/msys2.ps1 b/src-tauri/scripts/msys2.ps1 new file mode 100644 index 0000000..afc2f14 --- /dev/null +++ b/src-tauri/scripts/msys2.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference = "Stop" + +$target = $args[0] +$downloadUrl = "https://mirrors.tuna.tsinghua.edu.cn/msys2/distrib/msys2-x86_64-latest.sfx.exe" +$mirror = 'sed -i "s#https\?://mirror.msys2.org/#https://mirrors.tuna.tsinghua.edu.cn/msys2/#g" /etc/pacman.d/mirrorlist*' + +$installer = "$($env:TEMP)\msys2.exe" + + +if (!(Test-Path $installer -PathType Leaf)) { + Write-Host "Download file to $($installer)" + Invoke-WebRequest -Uri $downloadUrl -OutFile $installer +} + +& $installer "-y", "-o$($target)" + +$msys2 = "$($target)/msys64/usr/bin/bash.exe" + +& $msys2 "-l", "-c", $mirror +& $msys2 "-l", "-c", "pacman --noconfirm -S mingw-w64-ucrt-x86_64-clang-tools-extra mingw-w64-ucrt-x86_64-gcc" + +Write-Host "Tools were installed to $($target)/msys64/ucrt64/bin" + +$report = @{ + 'gcc' = "$($target)\msys64\ucrt64\bin\g++.exe" + 'clangd' = "$($target)\msys64\ucrt64\bin\clangd.exe" +} + +# $report | ConvertTo-Json | Out-File "$($target)/msys2.json" -Encoding UTF8NoBOM +$reportJson = $report | ConvertTo-Json +[IO.File]::WriteAllLines("$($target)\msys2.json", $reportJson) +Remove-Item $installer \ No newline at end of file diff --git a/src-tauri/src/ipc/cmd/bind.rs b/src-tauri/src/ipc/cmd/bind.rs index 6c9ada2..e7c6e0d 100644 --- a/src-tauri/src/ipc/cmd/bind.rs +++ b/src-tauri/src/ipc/cmd/bind.rs @@ -11,7 +11,7 @@ use tauri::Runtime; use tokio::sync::Mutex; pub struct LSPState { - t: Mutex)>>, + t: Mutex), (u16, Box)>>, } impl Default for LSPState { @@ -24,26 +24,29 @@ impl Default for LSPState { #[tauri::command] pub async fn get_lsp_server( - lsp_state: tauri::State<'_, LSPState>, + state: tauri::State<'_, LSPState>, mode: LanguageMode, + path: Option, ) -> Result { - let mut guard = lsp_state.t.lock().await; - if !guard.contains_key(&mode) { - match mode { + let mut guard = state.t.lock().await; + let key = (mode, path); + log::info!("start language server by {:?}", &key); + if !guard.contains_key(&key) { + match key.0 { LanguageMode::CXX => { - let mut server = ForwardServer::new(ClangdCommandBuilder); + let mut server = ForwardServer::new(ClangdCommandBuilder(key.1.clone())); let port = server.start().await.map_err(|e| e.to_string())?; - guard.insert(LanguageMode::CXX, (port, Box::new(server))); + guard.insert(key.clone(), (port, Box::new(server))); } LanguageMode::PY => { - let mut server = ForwardServer::new(PylsCommandBuilder); + let mut server = ForwardServer::new(PylsCommandBuilder(key.1.clone())); let port = server.start().await.map_err(|e| e.to_string())?; - guard.insert(LanguageMode::PY, (port, Box::new(server))); + guard.insert(key.clone(), (port, Box::new(server))); } } } - Ok(guard.get(&mode).unwrap().0) + Ok(guard.get(&key).unwrap().0) } #[tauri::command] diff --git a/src-tauri/src/ipc/cmd/host.rs b/src-tauri/src/ipc/cmd/host.rs new file mode 100644 index 0000000..ff56b18 --- /dev/null +++ b/src-tauri/src/ipc/cmd/host.rs @@ -0,0 +1,13 @@ +#[tauri::command] +pub async fn get_hostname() -> Result { + let name = gethostname::gethostname() + .to_string_lossy() + .as_ref() + .to_owned(); + Ok(name) +} + +#[tauri::command] +pub async fn get_system_name() -> Result { + Ok(std::env::consts::OS.to_owned()) +} diff --git a/src-tauri/src/ipc/cmd/mod.rs b/src-tauri/src/ipc/cmd/mod.rs index 277dd97..0e9f2dd 100644 --- a/src-tauri/src/ipc/cmd/mod.rs +++ b/src-tauri/src/ipc/cmd/mod.rs @@ -1,2 +1,3 @@ pub mod bind; +pub mod host; pub mod competitive_companion; diff --git a/src-tauri/src/ipc/lsp/clangd.rs b/src-tauri/src/ipc/lsp/clangd.rs index 03922e5..81b662b 100644 --- a/src-tauri/src/ipc/lsp/clangd.rs +++ b/src-tauri/src/ipc/lsp/clangd.rs @@ -2,13 +2,13 @@ use std::process::Command; use super::forward_server::LspCommandBuilder; -#[derive(Copy, Clone)] -pub struct ClangdCommandBuilder; +#[derive(Clone)] +pub struct ClangdCommandBuilder(pub Option); unsafe impl Sync for ClangdCommandBuilder {} unsafe impl Send for ClangdCommandBuilder {} impl LspCommandBuilder for ClangdCommandBuilder { fn build(&self) -> std::process::Command { - let mut cmd = Command::new("clangd"); + let mut cmd = Command::new(self.0.as_ref().map(|s|s.as_str()).unwrap_or("clangd")); cmd.args([ "--pch-storage=memory", "--clang-tidy", diff --git a/src-tauri/src/ipc/lsp/forward_server.rs b/src-tauri/src/ipc/lsp/forward_server.rs index 6e05b1e..ca95c92 100644 --- a/src-tauri/src/ipc/lsp/forward_server.rs +++ b/src-tauri/src/ipc/lsp/forward_server.rs @@ -21,7 +21,7 @@ use tokio_tungstenite::{ WebSocketStream, }; -pub trait LspCommandBuilder: Clone + Copy + Send + Sync { +pub trait LspCommandBuilder: Clone + Send + Sync { fn build(&self) -> Command; } diff --git a/src-tauri/src/ipc/lsp/pyrights.rs b/src-tauri/src/ipc/lsp/pyrights.rs index 54bfb81..d93a182 100644 --- a/src-tauri/src/ipc/lsp/pyrights.rs +++ b/src-tauri/src/ipc/lsp/pyrights.rs @@ -2,13 +2,13 @@ use std::process::Command; use super::forward_server::LspCommandBuilder; -#[derive(Copy, Clone)] -pub struct PylsCommandBuilder; +#[derive(Clone)] +pub struct PylsCommandBuilder(pub Option); unsafe impl Sync for PylsCommandBuilder {} unsafe impl Send for PylsCommandBuilder {} impl LspCommandBuilder for PylsCommandBuilder { fn build(&self) -> std::process::Command { - let mut cmd = Command::new("pyright-langserver"); + let mut cmd = Command::new(self.0.as_ref().map(|s| s.as_str()).unwrap_or("pyright-langserver")); cmd.args(["--stdio"]); cmd } diff --git a/src-tauri/src/ipc/mod.rs b/src-tauri/src/ipc/mod.rs index 1d42baa..09c2736 100644 --- a/src-tauri/src/ipc/mod.rs +++ b/src-tauri/src/ipc/mod.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Serialize}; +pub mod rt; pub mod cmd; pub mod lsp; -pub mod rt; +pub mod setup; -#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub enum LanguageMode { CXX, PY, diff --git a/src-tauri/src/ipc/rt/checker.rs b/src-tauri/src/ipc/rt/checker.rs index c383d15..47ecbd7 100644 --- a/src-tauri/src/ipc/rt/checker.rs +++ b/src-tauri/src/ipc/rt/checker.rs @@ -68,9 +68,10 @@ pub async fn check_answer( let checker_path = match checker { CheckerType::Internal { name } => if cfg!(windows) { app.path_resolver() - .resolve_resource(format!("{}.exe", name)) + .resolve_resource(format!("sidecar/{}.exe", name)) } else { - app.path_resolver().resolve_resource(name) + app.path_resolver() + .resolve_resource(format!("sidecar/{}", name)) } .ok_or(String::from("no such the checker"))?, _ => unimplemented!(), diff --git a/src-tauri/src/ipc/rt/compiler.rs b/src-tauri/src/ipc/rt/compiler.rs index ccbd03e..27f23ab 100644 --- a/src-tauri/src/ipc/rt/compiler.rs +++ b/src-tauri/src/ipc/rt/compiler.rs @@ -7,7 +7,7 @@ use tokio::sync::RwLock; use crate::{ipc::LanguageMode, util::keylock::KeyLock}; -use super::gnu_gcc::GNUGccCompiler; +use super::{gnu_gcc::GNUGccCompiler, interpreter::Interpreter}; #[derive(Debug, Serialize, Deserialize)] pub struct CompileLint { @@ -84,7 +84,7 @@ pub async fn compile_source( let compiler_handle: Box = match mode { LanguageMode::CXX => Box::new(GNUGccCompiler), - LanguageMode::PY => unimplemented!(), + LanguageMode::PY => Box::new(Interpreter::with_ext(String::from("py"))), }; let result = compiler_handle diff --git a/src-tauri/src/ipc/rt/gnu_gcc.rs b/src-tauri/src/ipc/rt/gnu_gcc.rs index 50cd226..f53f3ef 100644 --- a/src-tauri/src/ipc/rt/gnu_gcc.rs +++ b/src-tauri/src/ipc/rt/gnu_gcc.rs @@ -1,8 +1,14 @@ -use std::{path::PathBuf, process::Stdio}; +use std::{collections::HashMap, path::PathBuf, process::Stdio}; -use crate::{ipc::rt::compiler::CompileLint, util::console}; +use crate::{ + ipc::rt::compiler::CompileLint, + util::{append_env_var, console}, +}; -use super::compiler::{CompileResult, Compiler, CompilerOptions}; +use super::{ + compiler::{CompileResult, Compiler, CompilerOptions}, + runner::CompileDataStore, +}; use anyhow::{Ok, Result}; use async_trait::async_trait; use fancy_regex::{Regex, RegexBuilder}; @@ -27,12 +33,27 @@ impl Compiler for GNUGccCompiler { ) -> Result { let source_file = working_path.join("source.cpp"); fs::write(&source_file, source).await?; + + // init compiler, including set PATH var on windows let compiler = compiler_options.path.unwrap_or(String::from("g++")); - let mut cmd = std::process::Command::new(compiler); + let compiler = PathBuf::from(&compiler); + let path_var = if cfg!(windows) { + let compiler_dir = compiler.parent().unwrap(); + let path_var = append_env_var("PATH", compiler_dir.to_str().unwrap().to_owned()); + Some(path_var) + } else { + None + }; + let mut cmd = std::process::Command::new(&compiler); + if let Some(path_var) = &path_var { + cmd.env("PATH", path_var); + } + // hiden console on windows console::hide_new_console(&mut cmd); let mut cmd = Command::from(cmd); - let mut args = compiler_options.args.unwrap_or_else(|| Vec::new()); - let source_file_path = dunce::canonicalize(source_file)? + // construct compiler arguments + let mut args: Vec = compiler_options.args.unwrap_or_else(|| Vec::new()); + let source_file_path = dunce::canonicalize(&source_file)? .to_str() .unwrap() .to_owned(); @@ -43,13 +64,14 @@ impl Compiler for GNUGccCompiler { .to_owned(); target_file_path.push_str("/target"); - args.push(source_file_path); + args.push(source_file_path.clone()); args.push(String::from("-o")); args.push(target_file_path.clone()); + // prepare gain output cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); - cmd.args(args); + cmd.args(&args); let proc = cmd.spawn()?; let output = proc.wait_with_output().await?; @@ -57,6 +79,14 @@ impl Compiler for GNUGccCompiler { if cfg!(windows) { target_file_path.push_str(".exe"); } + let store = CompileDataStore { + compile: compiler.to_str().unwrap().to_owned(), + compile_args: args, + source: source_file_path, + required_env_running: path_var + .map(|v| HashMap::from_iter(vec![(String::from("PATH"), v)].into_iter())), + }; + store.save(&target_file_path).await?; Ok(CompileResult::Success { data: target_file_path, }) diff --git a/src-tauri/src/ipc/rt/interpreter.rs b/src-tauri/src/ipc/rt/interpreter.rs new file mode 100644 index 0000000..187376e --- /dev/null +++ b/src-tauri/src/ipc/rt/interpreter.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use crate::ipc::rt::compiler::CompileLint; + +use super::{ + compiler::{CompileResult, Compiler, CompilerOptions}, + runner::CompileDataStore, +}; +use anyhow::{Ok, Result}; +use async_trait::async_trait; +use tokio::fs; + +// It just record its compiler into a config file, not compile it +// it always success +pub struct Interpreter { + ext: String, +} +impl Interpreter { + pub fn with_ext(ext: String) -> Self { + Self { ext } + } +} + +#[async_trait] +impl Compiler for Interpreter { + async fn compile( + &self, + source: String, + compiler_options: CompilerOptions, + working_path: PathBuf, + ) -> Result { + let source_file = working_path.join(format!("source.{}", self.ext)); + let compiler = if let Some(compiler) = compiler_options.path { + compiler + } else { + return Ok(CompileResult::Error { + data: vec![CompileLint { + position: Some((0, 0)), + ty: Some(String::from("Error")), + description: String::from("An compiler or interpreter must be specificed"), + }], + }); + }; + let compiler_args = compiler_options.args.unwrap_or(vec![]); + let store = CompileDataStore { + compile: compiler, + source: source_file.to_str().unwrap().to_owned(), + compile_args: compiler_args, + required_env_running: None, + }; + fs::write(&source_file, source).await?; + store.save(&source_file).await?; + + Ok(CompileResult::Success { + data: source_file.to_str().unwrap().to_owned(), + }) + } +} diff --git a/src-tauri/src/ipc/rt/mod.rs b/src-tauri/src/ipc/rt/mod.rs index 02b4b1a..f06200f 100644 --- a/src-tauri/src/ipc/rt/mod.rs +++ b/src-tauri/src/ipc/rt/mod.rs @@ -1,5 +1,6 @@ +pub mod checker; pub mod compiler; +pub mod interpreter; pub mod runner; -pub mod checker; pub mod gnu_gcc; diff --git a/src-tauri/src/ipc/rt/runner.rs b/src-tauri/src/ipc/rt/runner.rs index dc2682b..6c84919 100644 --- a/src-tauri/src/ipc/rt/runner.rs +++ b/src-tauri/src/ipc/rt/runner.rs @@ -1,6 +1,7 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::process::{ExitStatus, Stdio}; use std::time::Duration; use tauri::Runtime; @@ -16,21 +17,97 @@ use crate::{ }, }; +#[derive(Debug, Serialize, Deserialize)] +pub struct CompileDataStore { + pub compile: String, + pub source: String, + pub compile_args: Vec, + pub required_env_running: Option>, +} +impl CompileDataStore { + fn get_addition_path>(src: P) -> PathBuf { + let mut ext = src + .as_ref() + .extension() + .map(|s| s.to_str().unwrap()) + .unwrap_or("") + .to_owned(); + ext.push_str(".toml"); + src.as_ref().with_extension(ext) + } + + pub async fn load>(src: P) -> Result { + let path = CompileDataStore::get_addition_path(src); + let content = tokio::fs::read_to_string(path).await?; + Ok(toml::from_str(&content)?) + } + + pub async fn save>(&self, src: P) -> Result<()> { + let path = CompileDataStore::get_addition_path(src); + let conetnt = toml::to_string(&self)?; + tokio::fs::write(&path, conetnt).await?; + Ok(()) + } + + pub fn with_env(&self, cmd: &mut std::process::Command) { + if let Some(env) = &self.required_env_running { + for (k, v) in env { + cmd.env(k, v); + } + } + } + + pub fn with_interpreter_command(&self, cmd: &mut std::process::Command) -> Result<()> { + cmd.arg(&self.compile); + cmd.arg(&self.source); + cmd.args(&self.compile_args); + + Ok(()) + } + pub fn as_interpreter_command(&self) -> std::process::Command { + let mut cmd = std::process::Command::new(&self.compile); + cmd.arg(&self.source); + cmd.args(&self.compile_args); + + cmd + } +} + #[tauri::command] pub async fn run_detach( app: tauri::AppHandle, + mode: LanguageMode, target: String, args: Vec, ) -> Result<(), String> { let pauser = if cfg!(windows) { - app.path_resolver().resolve_resource("consolepauser.exe") + app.path_resolver() + .resolve_resource("sidecar/consolepauser.exe") } else { - app.path_resolver().resolve_resource("consolepauser") + app.path_resolver() + .resolve_resource("sidecar/consolepauser") } .unwrap(); + let mut cmd = std::process::Command::new(pauser); - cmd.arg("1").arg(target).args(args); + cmd.arg("1"); create_new_console(&mut cmd); + let compile_data = CompileDataStore::load(&target) + .await + .map_err(|e| e.to_string())?; + match mode { + LanguageMode::CXX => { + cmd.arg(target); + } + LanguageMode::PY => { + compile_data + .with_interpreter_command(&mut cmd) + .map_err(|e| e.to_string())?; + } + } + compile_data.with_env(&mut cmd); + + cmd.args(&args); cmd.spawn().map_err(|e| e.to_string())?; Ok(()) @@ -152,10 +229,14 @@ pub async fn run_redirect( }; let working_dir = PathBuf::from(&exec_target).parent().unwrap().to_path_buf(); + let compile_data = CompileDataStore::load(PathBuf::from(&exec_target)) + .await + .map_err(|e| e.to_string())?; let mut cmd = match mode { LanguageMode::CXX => std::process::Command::new(exec_target), - _ => unimplemented!(), + LanguageMode::PY => compile_data.as_interpreter_command(), }; + compile_data.with_env(&mut cmd); cmd.current_dir(working_dir); let update = RunnerOutputUpdater::new(task_id.clone(), window); @@ -202,7 +283,7 @@ pub async fn run_redirect( Ok(result) } -struct ChildKiller(Child); +pub struct ChildKiller(Child); impl AsMut for ChildKiller { fn as_mut(&mut self) -> &mut Child { &mut self.0 diff --git a/src-tauri/src/ipc/setup/installer.rs b/src-tauri/src/ipc/setup/installer.rs new file mode 100644 index 0000000..fc04df1 --- /dev/null +++ b/src-tauri/src/ipc/setup/installer.rs @@ -0,0 +1,96 @@ +use std::process::{Command, Stdio}; + +use tauri::Runtime; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + sync::Mutex, +}; + +use crate::{util::console, RESOURCE_DIR}; + +#[derive(Default)] +pub struct PwshScriptState { + s: Mutex<()>, +} + +#[tauri::command] +pub async fn execuate_pwsh_script( + app: tauri::AppHandle, + window: tauri::Window, + state: tauri::State<'_, PwshScriptState>, + name: String, +) -> Result { + if !cfg!(windows) { + return Err(String::from("Installer only work on windows")); + } + let _guard = state.s.lock().await; + + let script_file = app + .path_resolver() + .resolve_resource(format!("sidecar/{}.ps1", &name)) + .unwrap(); + let script_file = dunce::canonicalize(script_file).unwrap(); + log::info!("execuate script {:?}", &script_file.to_str()); + let mut cmd = Command::new(which::which("powershell").unwrap()); + + let target = RESOURCE_DIR.get().unwrap(); + cmd.arg(&script_file.to_str().unwrap().replace(" ", "` ")); + cmd.arg(target.to_str().unwrap().replace(" ", "` ")); + console::hide_new_console(&mut cmd); + + let mut cmd = tokio::process::Command::from(cmd); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + cmd.stdin(Stdio::piped()); + let mut proc = cmd.spawn().unwrap(); + let mut stdout = BufReader::new( + proc.stdout + .take() + .ok_or(String::from("Fail to open stdout"))?, + ) + .lines(); + let mut stderr = BufReader::new( + proc.stderr + .take() + .ok_or(String::from("Fail to open stderr"))?, + ) + .lines(); + + let mut stdout_eof = false; + let mut stderr_eof = false; + let result = loop { + tokio::select! { + Ok(data) = stdout.next_line(), if !stdout_eof => { + if let Some(line) = data { + window.emit("install_message", line).map_err(|e| e.to_string())?; + }else{ + stdout_eof = true + } + } + Ok(data) = stderr.next_line(), if !stderr_eof => { + if let Some(line) = data{ + window.emit("install_message", line).map_err(|e| e.to_string())?; + }else{ + stderr_eof = true; + } + } + else => { + break proc.wait().await.map_err(|e|e.to_string())?; + } + } + }; + + if result.success() { + let report = target.join(format!("{}.json", name)); + let content = tokio::fs::read_to_string(report) + .await + .map_err(|e| e.to_string())?; + Ok(content) + } else { + if let Some(code) = result.code() { + Err(format!("Process exit with code {}", code)) + } else { + Err(String::from("Process exit with terminal signal")) + } + } +} diff --git a/src-tauri/src/ipc/setup/mod.rs b/src-tauri/src/ipc/setup/mod.rs new file mode 100644 index 0000000..d0cefca --- /dev/null +++ b/src-tauri/src/ipc/setup/mod.rs @@ -0,0 +1,43 @@ +pub mod installer; + +use std::{process::Stdio, time::Duration}; + +use serde::{Deserialize, Serialize}; +use tokio::process::Command; + +use crate::util::console; + +#[tauri::command] +pub async fn which(name: String) -> Result, String> { + Ok(which::which(name) + .ok() + .map(|p| p.to_str().unwrap().to_owned())) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OutputCapture { + exit_code: i32, + stdout: String, + stderr: String, +} + +#[tauri::command] +pub async fn capture_output(program: String, args: Vec) -> Result { + let program = which::which(program).map_err(|e| e.to_string())?; + let mut command = std::process::Command::new(&program); + command.args(&args); + console::hide_new_console(&mut command); + let mut command = Command::from(command); + command.stderr(Stdio::inherit()); + command.stdout(Stdio::inherit()); + let output = tokio::time::timeout(Duration::from_secs(3), command.output()) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string())?; + log::info!("capture output of {:?} {:?} :{:?}", &program, &args, output); + Ok(OutputCapture { + exit_code: output.status.code().unwrap(), + stdout: String::from_utf8_lossy(&output.stdout).as_ref().to_owned(), + stderr: String::from_utf8_lossy(&output.stderr).as_ref().to_owned(), + }) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 06a68ad..183d04d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,30 +6,22 @@ use std::{env, path::PathBuf}; use ipc::{ cmd::{bind::LSPState, competitive_companion::CompetitiveCompanionState}, rt::{checker::CheckerState, compiler::CompilerState, runner::RunnerState}, + setup::installer::PwshScriptState, }; use log::LevelFilter; -use once_cell::sync::{Lazy, OnceCell}; +use once_cell::sync::OnceCell; use tauri::{plugin::TauriPlugin, Manager, Runtime}; use tauri_plugin_log::{fern::colors::ColoredLevelConfig, LogTarget}; pub mod ipc; pub mod util; -static CURRENT_DIR: Lazy> = Lazy::new(|| { - let cell = OnceCell::new(); - cell.set(env::current_exe().unwrap().parent().unwrap().to_path_buf()) - .unwrap(); - cell -}); +pub static CONFIG_DIR: OnceCell = OnceCell::new(); +pub static RESOURCE_DIR: OnceCell = OnceCell::new(); fn log_pugin() -> TauriPlugin { - let log_dir = CURRENT_DIR.get().unwrap().join("logs"); let builder = tauri_plugin_log::Builder::default() - .targets([ - LogTarget::Stdout, - LogTarget::Webview, - LogTarget::Folder(log_dir), - ]) + .targets([LogTarget::Stdout, LogTarget::Webview, LogTarget::LogDir]) .with_colors(ColoredLevelConfig::default()) .max_file_size(10240); let builder = if cfg!(debug_assertions) { @@ -47,13 +39,7 @@ async fn is_debug() -> Result { #[tauri::command] async fn get_settings_path() -> Result { - Ok(CURRENT_DIR - .get() - .unwrap() - .join("settings.dat") - .to_str() - .unwrap() - .to_owned()) + Ok(String::from("settings.dat")) } fn main() { @@ -61,12 +47,21 @@ fn main() { .plugin(log_pugin()) .plugin(tauri_plugin_store::Builder::default().build()) .setup(|app| { - // provlegisto statue + // provlegisto state + CONFIG_DIR + .set(dunce::canonicalize(app.path_resolver().app_config_dir().unwrap()).unwrap()) + .unwrap(); + RESOURCE_DIR + .set( + dunce::canonicalize(app.path_resolver().app_local_data_dir().unwrap()).unwrap(), + ) + .unwrap(); app.manage(LSPState::default()); app.manage(CompetitiveCompanionState::default()); app.manage(CompilerState::default()); app.manage(RunnerState::default()); app.manage(CheckerState::default()); + app.manage(PwshScriptState::default()); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -76,6 +71,11 @@ fn main() { ipc::cmd::bind::open_devtools, ipc::cmd::competitive_companion::enable_competitive_companion, ipc::cmd::competitive_companion::disable_competitive_companion, + ipc::cmd::host::get_hostname, + ipc::cmd::host::get_system_name, + ipc::setup::capture_output, + ipc::setup::which, + ipc::setup::installer::execuate_pwsh_script, ipc::rt::compiler::compile_source, ipc::rt::runner::run_detach, ipc::rt::runner::run_redirect, diff --git a/src-tauri/src/util/mod.rs b/src-tauri/src/util/mod.rs index ee6a445..32b9a77 100644 --- a/src-tauri/src/util/mod.rs +++ b/src-tauri/src/util/mod.rs @@ -1,3 +1,18 @@ pub mod console; pub mod error; pub mod keylock; + +pub fn append_env_var(name: &str, value: String) -> String { + let origin = std::env::var(name); + if let Ok(mut origin) = origin { + if cfg!(windows) { + origin.push(';'); + }else{ + origin.push(':'); + } + origin.push_str(&value); + origin + }else{ + value + } +} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bcc734e..d738697 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -33,7 +33,14 @@ "identifier": "com.mslxl.provlegisto", "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "resources": { - "src-util/build/*": "." + "src-util/build/*": "./sidecar/", + "scripts/*": "./sidecar/" + }, + "windows": { + "webviewInstallMode": { + "silent": true, + "type": "embedBootstrapper" + } } }, "security": { diff --git a/src/components/codemirror/index.tsx b/src/components/codemirror/index.tsx index 7b6ddfd..9dac7b1 100644 --- a/src/components/codemirror/index.tsx +++ b/src/components/codemirror/index.tsx @@ -12,7 +12,7 @@ import { useImmerAtom } from "jotai-immer" import { map } from "lodash" import "@fontsource/jetbrains-mono" import { filterCSSQuote } from "@/lib/utils" -import { editorFontFamily, editorFontSizeAtom } from "@/store/setting" +import { editorFontFamily, editorFontSizeAtom } from "@/store/setting/ui" type CodemirrorProps = { className?: string @@ -30,11 +30,10 @@ function useExtensionCompartment( const value = useAtomValue(atom) useEffect(() => { if (cm.current == null) return - console.log("dispatch") cm.current.dispatch({ effects: compartment.current.reconfigure(builder(value)), }) - }, [atom, cm]) + }, [atom, cm, value]) return () => compartment.current.of(builder(value)) } diff --git a/src/components/codemirror/language.ts b/src/components/codemirror/language.ts index c1a5186..f0be75d 100644 --- a/src/components/codemirror/language.ts +++ b/src/components/codemirror/language.ts @@ -2,15 +2,15 @@ import { languageServer } from "codemirror-languageserver" import { Extension } from "@codemirror/state" import { LanguageMode, getLSPServer } from "@/lib/ipc" -export type LspProvider = () => Promise<() => Extension> +export type LspProvider = (ls: string) => Promise<() => Extension> export const noLsp: LspProvider = async () => { return () => [] } -export const cxxLsp: LspProvider = async (): Promise<() => Extension> => { +export const cxxLsp: LspProvider = async (ls: string): Promise<() => Extension> => { const highlight = await import("@codemirror/lang-cpp") - const serverPort = await getLSPServer(LanguageMode.CXX) + const serverPort = await getLSPServer(LanguageMode.CXX, ls) const serverUri: any = `ws://127.0.0.1:${serverPort}` return () => [ @@ -25,11 +25,10 @@ export const cxxLsp: LspProvider = async (): Promise<() => Extension> => { ] } -export const pyLsp: LspProvider = async (): Promise<() => Extension> => { +export const pyLsp: LspProvider = async (pyrights: string): Promise<() => Extension> => { const highlight = await import("@codemirror/lang-python") - const serverPort = await getLSPServer(LanguageMode.PY) + const serverPort = await getLSPServer(LanguageMode.PY, pyrights) const serverUri: any = `ws://127.0.0.1:${serverPort}` - return () => [ highlight.python(), languageServer({ diff --git a/src/components/pref/Item.tsx b/src/components/pref/Item.tsx index 73191c4..4f733e9 100644 --- a/src/components/pref/Item.tsx +++ b/src/components/pref/Item.tsx @@ -1,18 +1,22 @@ +import clsx from "clsx" import { ReactNode } from "react" export type PrefItemProps = { leading: string description?: string msg?: string | null + className?: string children: ReactNode -} +} export default function PrefItem(props: PrefItemProps) { return (
-

{props.leading}

-

{props.description}

-
{props.children}
+
+

{props.leading}

+

{props.description}

+
+
{props.children}
{props.msg ? {props.msg} : null}
) diff --git a/src/components/pref/Program.tsx b/src/components/pref/Program.tsx new file mode 100644 index 0000000..6407f53 --- /dev/null +++ b/src/components/pref/Program.tsx @@ -0,0 +1,68 @@ +import { Atom, PrimitiveAtom, useAtom, useAtomValue } from "jotai" +import PrefItem, { PrefItemProps } from "./Item" +import { Input } from "../ui/input" +import { Button } from "../ui/button" +import { ReactNode, Suspense } from "react" +import clsx from "clsx" +import { dialog } from "@tauri-apps/api" +import { Skeleton } from "../ui/skeleton" + +interface DialogFilter { + name: string + extensions: string[] +} +type PrefProgramProps = { + valueAtom: PrimitiveAtom + versionAtom: Atom + versionFallback: string + dialogFilter?: DialogFilter[] + children?: ReactNode +} & { + [Property in keyof PrefItemProps as Exclude]: PrefItemProps[Property] +} + +function Msg({ atom, fallback }: { atom: Atom; fallback: string }) { + const value = useAtomValue(atom) + return ( + + {value ?? fallback} + + ) +} + +export default function PrefProgram(props: PrefProgramProps) { + const [path, setPath] = useAtom(props.valueAtom) + + async function choosePath() { + let userChooseFile = await dialog.open({ + multiple: false, + filters: props.dialogFilter, + }) + if (userChooseFile == null) return null + let file = userChooseFile as string + setPath(file) + } + + return ( +
+ + setPath(e.target.value)} + onKeyDown={(e) => { + if (e.key == "Enter") setPath((e.target as any).value) + }} + /> + + {props.children} + + }> + + +
+ ) +} diff --git a/src/components/pref/Select.tsx b/src/components/pref/Select.tsx index 6de7e88..dbba869 100644 --- a/src/components/pref/Select.tsx +++ b/src/components/pref/Select.tsx @@ -4,7 +4,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". import { PrimitiveAtom, useAtom } from "jotai" import { map } from "lodash" -type PrefSelectItem = { +export type PrefSelectItem = { key: string value: string } diff --git a/src/components/runner/empty.tsx b/src/components/runner/empty.tsx new file mode 100644 index 0000000..62911e0 --- /dev/null +++ b/src/components/runner/empty.tsx @@ -0,0 +1,26 @@ +import { emit } from "@/hooks/useMitt" +import clsx from "clsx" +import styled from "styled-components" + +type EmptyRunnerProps = { + className?: string +} +const ClickableLink = styled.a` +cursor: pointer; +text-decoration: underline; +` +export default function EmptyRunner(props: EmptyRunnerProps) { + return ( +
+
No Test Case
+
+ Use + + {" "} + Competitive Companion{" "} + + or emit("fileMenu", "new")}> Create new file to continue +
+
+ ) +} diff --git a/src/components/runner/index.tsx b/src/components/runner/index.tsx index b1279e7..2e70e0e 100644 --- a/src/components/runner/index.tsx +++ b/src/components/runner/index.tsx @@ -17,11 +17,13 @@ import { focusAtom } from "jotai-optics" import { splitAtom } from "jotai/utils" import { emptyTestcase } from "@/store/testcase" import useReadAtom from "@/hooks/useReadAtom" +import useGetLanguageCompiler from "@/hooks/useGetLanguageCompiler" +import EmptyRunner from "./empty" export default function Runnner({ className }: { className?: string }) { const activeId = useAtomValue(activeIdAtom) if (activeId == -1) { - return
+ return } return } @@ -44,10 +46,15 @@ function RunnerContent(props: { className?: string; activeIdAtom: Atom } const [runAllAnimate, setRunAllAnimate] = useState(false) + const getLanguageCompilerPath = useGetLanguageCompiler() + async function runAll() { setRunAllAnimate(true) const sourceCode = readSourceCode() - await compileSource(sourceCode.language, sourceCode.source) + await compileSource(sourceCode.language, sourceCode.source, { + path: (await getLanguageCompilerPath(sourceCode.language)) ?? undefined, + args: [], + }) setRunAllAnimate(false) } @@ -62,20 +69,18 @@ function RunnerContent(props: { className?: string; activeIdAtom: Atom } async function onRunDetachClick() { setRunAllAnimate(true) const sourceCode = readSourceCode() - let target = await compileSource(sourceCode.language, sourceCode.source) + let target = await compileSource(sourceCode.language, sourceCode.source, { + path: (await getLanguageCompilerPath(sourceCode.language)) ?? undefined, + }) setRunAllAnimate(false) if (target.type === "Success") { - runDetach(target.data) + runDetach(target.data, sourceCode.language) } else { log.warn(JSON.stringify(target)) } } - if (activeId == -1) { - return
TODO
- } - const testcaseList = testcases.map((atom, index) => ( sourceAtom: Atom @@ -74,6 +75,7 @@ export default function SingleRunner(props: SingleRunnerProps) { const acutalStdoutLinesCnt = useRef(0) const [running, setRunning] = useState(false) const [checkerReport, setCheckerReport] = useState("") + const getLanguageCompiler = useGetLanguageCompiler() useEffect(() => { setJudgeStatus("UK") @@ -82,7 +84,7 @@ export default function SingleRunner(props: SingleRunnerProps) { useMitt( "run", - (taskId) => { + async (taskId) => { if (props.taskId != taskId && taskId != "all") return setRunning(true) setJudgeStatus("PD") @@ -95,11 +97,22 @@ export default function SingleRunner(props: SingleRunnerProps) { const checker = readChecker() log.info(`compile in ${sourceCode.language} mode with checker ${checker}`) - - compileRunCheck(sourceCode.language, sourceCode.source, props.taskId, input, output, { - type: "Internal", - name: checker, - }, readTimeLimits()) + + compileRunCheck( + sourceCode.language, + sourceCode.source, + props.taskId, + input, + output, + { + type: "Internal", + name: checker, + }, + readTimeLimits(), + { + path: (await getLanguageCompiler(sourceCode.language)) ?? undefined, + }, + ) .then((result) => { log.info(JSON.stringify(result)) @@ -111,7 +124,7 @@ export default function SingleRunner(props: SingleRunnerProps) { line += `${i.ty} ${i.position[0]}:${i.position[1]} ${i.description}\n` } setCheckerReport(line) - }else { + } else { setCheckerReport(" ") } diff --git a/src/components/setup/setup-cxx.tsx b/src/components/setup/setup-cxx.tsx new file mode 100644 index 0000000..4a39d93 --- /dev/null +++ b/src/components/setup/setup-cxx.tsx @@ -0,0 +1,60 @@ +import PrefProgram from "@/components/pref/Program" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { SystemName } from "@/lib/ipc/host" +import { clangdPathAtom, clangdVersionAtom, enableCxxAtom, gccPathAtom, gccVersionAtom } from "@/store/setting/setup" +import { systemNameAtom } from "@/store/system" +import { useAtom, useAtomValue } from "jotai" +import { useNavigate } from "react-router-dom" + +export default function SetupCXX() { + const [enableCxx, setEnableCxx] = useAtom(enableCxxAtom) + const systemName = useAtomValue(systemNameAtom) + const navigate = useNavigate() + const btnInstall = ( + + + + + + +

Only work on windows

+

Download MSYS2 and install it automatically

+
+
+
+ ) + return ( +
+
+ setEnableCxx(e)} /> + +
+ {!enableCxx ? null : ( + <> + + {btnInstall} + + + {btnInstall} + + + )} +
+ ) +} diff --git a/src/components/setup/setup-py.tsx b/src/components/setup/setup-py.tsx new file mode 100644 index 0000000..09713f4 --- /dev/null +++ b/src/components/setup/setup-py.tsx @@ -0,0 +1,44 @@ +import PrefProgram from "@/components/pref/Program" +import { Checkbox } from "@/components/ui/checkbox" +import { + enablePythonAtom, + pyrightsPathAtom, + pyrightsVersionAtom, + pythonPathAtom, + pythonVersionAtom, +} from "@/store/setting/setup" +import { useAtom } from "jotai" + +export default function SetupPy() { + const [enablePy, setEnablePy] = useAtom(enablePythonAtom) + return ( +
+
+ setEnablePy(e)} /> + +
+ {!enablePy ? null : ( + <> + + {/* */} + + + {/* */} + + + )} +
+ ) +} diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..cc49f39 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..ddbdd01 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..105fb65 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..01b8b6d --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..e121f0a --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/hooks/useChangeLanguageDialog.tsx b/src/hooks/useChangeLanguageDialog.tsx new file mode 100644 index 0000000..0e24afe --- /dev/null +++ b/src/hooks/useChangeLanguageDialog.tsx @@ -0,0 +1,34 @@ +import PrefSelect from "@/components/pref/Select" +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { availableLanguageListAtom } from "@/store/setting/setup" +import { PrimitiveAtom, useAtomValue } from "jotai" +import { ReactNode, useState } from "react" + +export default function useChangeLanguageDialog(atom: PrimitiveAtom): [ReactNode, () => void, boolean] { + const [dialogOpen, setDialogOpen] = useState(false) + const available = useAtomValue(availableLanguageListAtom) + + function showDialog() { + setDialogOpen(true) + } + + let element = ( + + + + Change Language + +
+ + + + +
+
+
+ ) + return [element, showDialog, dialogOpen] +} diff --git a/src/hooks/useGetLanguageCompiler.ts b/src/hooks/useGetLanguageCompiler.ts new file mode 100644 index 0000000..311e044 --- /dev/null +++ b/src/hooks/useGetLanguageCompiler.ts @@ -0,0 +1,16 @@ +import { gccPathAtom, pythonPathAtom } from "@/store/setting/setup" +import useReadAtom from "./useReadAtom" +import { LanguageMode } from "@/lib/ipc" + +export default function useGetLanguageCompiler(): (lang: LanguageMode) => Promise { + const readGccPath = useReadAtom(gccPathAtom) + const readPyPath = useReadAtom(pythonPathAtom) + + return (lang): Promise => { + if (lang == LanguageMode.CXX) return readGccPath() as any + else if (lang == LanguageMode.PY) return readPyPath() as any + return new Promise((resolve) => { + resolve(null) + }) + } +} diff --git a/src/hooks/useZoom.ts b/src/hooks/useZoom.ts index b11caa6..bc92c0f 100644 --- a/src/hooks/useZoom.ts +++ b/src/hooks/useZoom.ts @@ -1,4 +1,4 @@ -import { zoomStateAtom } from "@/store/setting" +import { zoomStateAtom } from "@/store/setting/ui" import { useAtom } from "jotai" import { useEffect } from "react" diff --git a/src/lib/fs/installer.ts b/src/lib/fs/installer.ts new file mode 100644 index 0000000..5fbdeab --- /dev/null +++ b/src/lib/fs/installer.ts @@ -0,0 +1,9 @@ +import { execuatePwshScript } from "../ipc/setup" + +type Msys2Report = { + gcc: string + clangd: string +} +export async function installMsys2(): Promise { + return execuatePwshScript("msys2").then((res) => JSON.parse(res)) +} diff --git a/src/lib/ipc/host.ts b/src/lib/ipc/host.ts new file mode 100644 index 0000000..ad57cbf --- /dev/null +++ b/src/lib/ipc/host.ts @@ -0,0 +1,12 @@ +import { invoke } from "@tauri-apps/api" + +export const getHostname: () => Promise = () => invoke("get_hostname") + +export enum SystemName { + linux = "linux", + windows = "windows", + macos = "macos", + unknown = "", +} + +export const getSystemName: () => Promise = () => invoke("get_system_name") diff --git a/src/lib/ipc/index.ts b/src/lib/ipc/index.ts index 38357f7..959cc06 100644 --- a/src/lib/ipc/index.ts +++ b/src/lib/ipc/index.ts @@ -12,4 +12,4 @@ export const openDevTools = (): Promise => invoke("open_devtools") export const getSettingsPath = (): Promise => invoke("get_settings_path") -export const isDebug = (): Promise => invoke("is_debug") \ No newline at end of file +export const isDebug = (): Promise => invoke("is_debug") diff --git a/src/lib/ipc/lsp.ts b/src/lib/ipc/lsp.ts index c1df7de..a953743 100644 --- a/src/lib/ipc/lsp.ts +++ b/src/lib/ipc/lsp.ts @@ -1,7 +1,8 @@ import { invoke } from "@tauri-apps/api" import { LanguageMode } from "." -export const getLSPServer = (mode: LanguageMode): Promise => +export const getLSPServer = (mode: LanguageMode, path?: string): Promise => invoke("get_lsp_server", { mode, + path, }) diff --git a/src/lib/ipc/rt/runner.ts b/src/lib/ipc/rt/runner.ts index cc45eb4..5f2ee32 100644 --- a/src/lib/ipc/rt/runner.ts +++ b/src/lib/ipc/rt/runner.ts @@ -1,10 +1,10 @@ - import { invoke } from "@tauri-apps/api" import { LanguageMode } from "@/lib/ipc" -export const runDetach = (target: string, args?: string[]): Promise => +export const runDetach = (target: string, mode: LanguageMode, args?: string[]): Promise => invoke("run_detach", { target, + mode, args: args ?? [], }) @@ -39,4 +39,4 @@ export const runRedirect = ( input, timeout: timeout ?? 3000, additionArgs: additionArgs ?? [], - }) \ No newline at end of file + }) diff --git a/src/lib/ipc/setup.ts b/src/lib/ipc/setup.ts new file mode 100644 index 0000000..f83496d --- /dev/null +++ b/src/lib/ipc/setup.ts @@ -0,0 +1,16 @@ +import { invoke } from "@tauri-apps/api" + +export const whichBinary: (name: string) => Promise = (name) => invoke("which", { name: name }) + +export type OutputCapture = { + exitCode: number + stdout: string + stderr: string +} +export const captureOutput: (program: string, args: string[]) => Promise = (program, args) => + invoke("capture_output", { + program, + args, + }) + +export const execuatePwshScript = (name: string) => invoke("execuate_pwsh_script", { name }) diff --git a/src/pages/About/contributer.tsx b/src/pages/About/contributer.tsx index a9082b9..918840a 100644 --- a/src/pages/About/contributer.tsx +++ b/src/pages/About/contributer.tsx @@ -28,7 +28,7 @@ const contributers: Contributer[] = [ name: "Galong", avatar: "https://avatars.githubusercontent.com/u/94678496?v=4", fallback: "G", - ty: "Advice", + ty: "Advice & Debug", profile: "https://github.com/gjh303987897", }, ] diff --git a/src/pages/Install/index.tsx b/src/pages/Install/index.tsx new file mode 100644 index 0000000..3ad5b31 --- /dev/null +++ b/src/pages/Install/index.tsx @@ -0,0 +1,98 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, +} from "@/components/ui/alert-dialog" +import { Progress } from "@/components/ui/progress" +import { useTauriEvent } from "@/hooks/useTauriEvent" +import { installMsys2 } from "@/lib/fs/installer" +import { clangdPathAtom, gccPathAtom } from "@/store/setting/setup" +import { AlertDialogTitle } from "@radix-ui/react-alert-dialog" +import { useSetAtom } from "jotai" +import { useEffect, useRef, useState } from "react" +import { VscClose } from "react-icons/vsc" +import { useNavigate, useParams } from "react-router-dom" + +export default function Install() { + const params = useParams() + const installName = params.name + const navigate = useNavigate() + const setGcc = useSetAtom(gccPathAtom) + const setClangd = useSetAtom(clangdPathAtom) + const [fatal, setFatal] = useState(false) + const logRef = useRef(null) + + const [dialogMessage, setDialogMessage] = useState("") + const [dialogVisible, setDialogVisible] = useState(false) + const [output, setOutput] = useState("") + const running = useRef(false) + + useEffect(() => { + if (running.current) return + running.current = true + if (params.name == "msys2") { + installMsys2() + .then((res) => { + setGcc(res.gcc) + setClangd(res.clangd) + setDialogMessage("Install complete!") + setDialogVisible(true) + }) + .catch((e) => { + setDialogMessage(e) + setDialogVisible(true) + setFatal(true) + }) + } else { + setDialogMessage("No such installer") + setDialogVisible(true) + } + }, []) + + useTauriEvent( + "install_message", + (msg) => { + setOutput((pre) => `${pre}\n${msg.payload}`) + if(logRef.current == null) return + if(logRef.current.scrollTop + logRef.current.offsetHeight >= logRef.current.scrollHeight - 20){ + logRef.current.scrollTo(0, logRef.current.scrollHeight); + } + }, + [setOutput], + ) + + return ( +
+ + + + {fatal ? "Error" : "Message"} + {dialogMessage} + + + {!fatal ? null : See Logs} + navigate(-1)}>Back + + + +
+ {!fatal ? null : ( + + )} +

Installing {installName}

+

DO NOT close the application, the installer is running background

+

Girl in Prayer...

+
+ +
+
{output}
+
+
+ ) +} diff --git a/src/pages/Main/editor-tabpane.tsx b/src/pages/Main/editor-tabpane.tsx index 858bd92..ffe2c4a 100644 --- a/src/pages/Main/editor-tabpane.tsx +++ b/src/pages/Main/editor-tabpane.tsx @@ -1,8 +1,11 @@ import Codemirror from "@/components/codemirror" import { cxxLsp, noLsp, pyLsp } from "@/components/codemirror/language" +import useReadAtom from "@/hooks/useReadAtom" import { LanguageMode } from "@/lib/ipc" import { keymapExtensionAtom } from "@/store/setting/keymap" +import { clangdPathAtom, pyrightsPathAtom } from "@/store/setting/setup" import { SourceHeader, activeIdAtom, sourceStoreAtom } from "@/store/source" +import { Extension } from "@codemirror/state" import clsx from "clsx" import { PrimitiveAtom, atom, useAtomValue } from "jotai" import { focusAtom } from "jotai-optics" @@ -22,11 +25,14 @@ export default function EditorTabPanel(props: EditorProps) { ) const sourceCodeLanguage = useAtomValue(sourceCodeLanguageAtom) + const readClangdPath = useReadAtom(clangdPathAtom) + const readPyrightsPath = useReadAtom(pyrightsPathAtom) + const lspExtensionAtom = useMemo(() => { - return atom(() => { - if (sourceCodeLanguage == LanguageMode.CXX) return cxxLsp() - else if (sourceCodeLanguage == LanguageMode.PY) return pyLsp() - return noLsp() + return atom(async():Promise<()=>Extension> => { + if (sourceCodeLanguage == LanguageMode.CXX) return cxxLsp(await readClangdPath()) + else if (sourceCodeLanguage == LanguageMode.PY) return pyLsp(await readPyrightsPath()) + return noLsp("") }) }, [sourceCodeLanguage]) return ( diff --git a/src/pages/Main/index.tsx b/src/pages/Main/index.tsx index e817e18..aef1e68 100644 --- a/src/pages/Main/index.tsx +++ b/src/pages/Main/index.tsx @@ -11,19 +11,30 @@ import { sourceIndexAtomAtoms } from "@/store/source" import EditorTabPanel from "./editor-tabpane" import Runner from "@/components/runner" import MenuEventReceiver from "./menu-event" +import { hostnameAtom, setupDeviceAtom } from "@/store/setting/setup" +import { useNavigate } from "react-router-dom" +import * as log from "tauri-plugin-log-api" +import { useEffect } from "react" export default function Main() { useZoom() - + const hostname = useAtomValue(hostnameAtom) + const setupHostname = useAtomValue(setupDeviceAtom) + const navgiate = useNavigate() const [activePrimaryPanel] = useAtom(primaryPanelShowAtom) const [showStatusBar] = useAtom(statusBarShowAtom) const sourceIndexAtom = useAtomValue(sourceIndexAtomAtoms) + useEffect(() => { + log.info(`hostname: ${hostname}`) + log.info(`setupHostname: ${setupHostname}`) + if (setupHostname != hostname) navgiate("/setup") + }, [hostname, setupHostname]) return (
- +
- +
diff --git a/src/pages/Main/menu-event.tsx b/src/pages/Main/menu-event.tsx index 1beb247..198f717 100644 --- a/src/pages/Main/menu-event.tsx +++ b/src/pages/Main/menu-event.tsx @@ -1,8 +1,17 @@ import { useMitt } from "@/hooks/useMitt" import useReadAtom from "@/hooks/useReadAtom" import { openProblem, saveProblem } from "@/lib/fs/problem" -import { activeIdAtom, counterAtom, emptySource, sourceIndexAtomAtoms, sourceIndexAtoms, sourceStoreAtom, useAddSource } from "@/store/source" -import { useAtom, useSetAtom } from "jotai" +import { defaultLanguageAtom } from "@/store/setting/setup" +import { + activeIdAtom, + counterAtom, + emptySource, + sourceIndexAtomAtoms, + sourceIndexAtoms, + sourceStoreAtom, + useAddSource, +} from "@/store/source" +import { useAtom, useAtomValue, useSetAtom } from "jotai" import { useImmerAtom } from "jotai-immer" export default function MenuEventReceiver() { @@ -13,10 +22,11 @@ export default function MenuEventReceiver() { const readActiveId = useReadAtom(activeIdAtom) const readSourceIndex = useReadAtom(sourceIndexAtoms) const addSource = useAddSource() + const defaultLanguage = useAtomValue(defaultLanguageAtom) useMitt("fileMenu", async (event) => { if (event == "new") { - addSource("Unamed", emptySource()) + addSource("Unamed", emptySource(defaultLanguage!)) } else if (event == "open") { const problems = await openProblem() for (let i = 0; i < problems.length; i++) { diff --git a/src/pages/Main/sidebar.tsx b/src/pages/Main/sidebar.tsx index e038977..2d9a02d 100644 --- a/src/pages/Main/sidebar.tsx +++ b/src/pages/Main/sidebar.tsx @@ -15,11 +15,12 @@ import { emit } from "@/hooks/useMitt" import { useAtom } from "jotai" import { ReactNode } from "react" import { openDevTools } from "@/lib/ipc" -import { Link } from "react-router-dom" +import { Link, useNavigate } from "react-router-dom" import clsx from "clsx" export default function PrimarySide() { const [panel, setPanel] = useAtom(primaryPanelShowAtom) + const navigate = useNavigate() function onPanelButtonClick(panelId: string) { if (panelId == panel) setPanel(null) @@ -29,8 +30,8 @@ export default function PrimarySide() { const panelBtn = ( [ ["run", ], - ["team", ], - ["version", ], + ["team", ], + ["version", ], ] as [string, ReactNode][] ).map((item, index) => ( File emit("fileMenu", "new")}>New File - New Contest + {/* New Contest */} emit("fileMenu", "open")}>Open File - Open Contest - + {/* Open Contest */} + {/* Open Recent - {/* TODO: List here */} + TODO: List here More Clear Recently Open - + */} emit("fileMenu", "save")}>Save emit("fileMenu", "saveAs")}>Save As... @@ -92,7 +93,7 @@ export default function PrimarySide() { {panelBtn} - + navigate("/pref")}> diff --git a/src/pages/Main/tabbar.tsx b/src/pages/Main/tabbar.tsx index da8d0b0..5562b57 100644 --- a/src/pages/Main/tabbar.tsx +++ b/src/pages/Main/tabbar.tsx @@ -1,5 +1,5 @@ import clsx from "clsx" -import { useEffect, useRef } from "react" +import { useEffect, useMemo, useRef } from "react" import { VscAdd, VscClose } from "react-icons/vsc" import { useDrag, useDrop } from "react-dnd" import { useHoverDirty, useMouseWheel } from "react-use" @@ -12,8 +12,26 @@ import { ContextMenuTrigger, } from "@/components/ui/context-menu" import { useRenameDialog } from "../../hooks/useRenameDialog" -import { SourceHeader, activeIdAtom, emptySource, sourceIndexAtomAtoms, useAddSource } from "@/store/source" -import { PrimitiveAtom, useAtom } from "jotai" +import { + SourceHeader, + activeIdAtom, + emptySource, + sourceIndexAtomAtoms, + sourceStoreAtom, + useAddSource, +} from "@/store/source" +import { PrimitiveAtom, useAtom, useAtomValue } from "jotai" +import { focusAtom } from "jotai-optics" +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import useChangeLanguageDialog from "@/hooks/useChangeLanguageDialog" +import { defaultLanguageAtom } from "@/store/setting/setup" const HorizontalUnorderedList = styled.ul` position: relative; @@ -38,6 +56,7 @@ export default function Tabbar({ className }: { className: string }) { const mouseWheel = useMouseWheel() const lastWheel = useRef(0) const ulHover = useHoverDirty(ulRef) + const defaultLanguage = useAtomValue(defaultLanguageAtom) useEffect(() => { let delta = mouseWheel - lastWheel.current lastWheel.current = mouseWheel @@ -63,7 +82,7 @@ export default function Tabbar({ className }: { className: string }) { ))}
  • -
  • @@ -86,7 +105,10 @@ function Bar({ const ref = useRef(null) const [content, setContent] = useAtom(atom) const [activeId, setActiveId] = useAtom(activeIdAtom) - + const currentLanguageAtom = useMemo( + () => focusAtom(sourceStoreAtom, (optic) => optic.prop(content.id).prop("code").prop("language")), + [atom, content], + ) const [, dragRef] = useDrag({ type: "tabbox", item: { atom }, @@ -103,17 +125,30 @@ function Bar({ }, }) - const [dialog, showDialog] = useRenameDialog((value) => { + const [renameDialog, showRenameDialog] = useRenameDialog((value) => { setContent((e) => ({ ...e, title: value, })) }) + const [changeLanguageDialog, showChangeLanguageDialog] = useChangeLanguageDialog(currentLanguageAtom as any) dragRef(dropRef(ref)) return ( - {dialog} + {renameDialog} + {changeLanguageDialog} + + + + + + Change Language + + + + + - showDialog(content.title)}>Rename + showRenameDialog(content.title)}>Rename Close Copy Path + + showChangeLanguageDialog()}>Change Language ) diff --git a/src/pages/Preference/appearance.tsx b/src/pages/Preference/appearance.tsx index 01637b4..7bacd7c 100644 --- a/src/pages/Preference/appearance.tsx +++ b/src/pages/Preference/appearance.tsx @@ -1,5 +1,5 @@ import { PrefNumber } from "@/components/pref" -import { zoomStateAtom } from "@/store/setting" +import { zoomStateAtom } from "@/store/setting/ui" export default function Page() { return ( diff --git a/src/pages/Preference/editor.tsx b/src/pages/Preference/editor.tsx index 2520130..a1b547d 100644 --- a/src/pages/Preference/editor.tsx +++ b/src/pages/Preference/editor.tsx @@ -1,7 +1,7 @@ import { PrefNumber } from "@/components/pref" import { PrefText } from "@/components/pref/Text" import { filterCSSQuote } from "@/lib/utils" -import { editorFontFamily, editorFontSizeAtom } from "@/store/setting" +import { editorFontFamily, editorFontSizeAtom } from "@/store/setting/ui" import { useAtomValue } from "jotai" import styled from "styled-components" diff --git a/src/pages/Preference/index.tsx b/src/pages/Preference/index.tsx index 220260a..56a74e4 100644 --- a/src/pages/Preference/index.tsx +++ b/src/pages/Preference/index.tsx @@ -10,13 +10,13 @@ export default function Preference() { const navigate = useNavigate() return (
    -
  • - Appearance + Appearance
  • Editor @@ -24,6 +24,9 @@ export default function Preference() {
  • Keymap
  • +
  • + Language +
  • }> diff --git a/src/pages/Preference/language.tsx b/src/pages/Preference/language.tsx new file mode 100644 index 0000000..3ca8f15 --- /dev/null +++ b/src/pages/Preference/language.tsx @@ -0,0 +1,26 @@ +import PrefSelect from "@/components/pref/Select" +import SetupCXX from "@/components/setup/setup-cxx" +import SetupPy from "@/components/setup/setup-py" +import { availableLanguageListAtom, defaultLanguageAtom } from "@/store/setting/setup" +import { useAtomValue } from "jotai" + +export default function Page() { + const availableLanguageList = useAtomValue(availableLanguageListAtom) + return ( +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    + ) +} diff --git a/src/pages/Setup/index.tsx b/src/pages/Setup/index.tsx new file mode 100644 index 0000000..142a94b --- /dev/null +++ b/src/pages/Setup/index.tsx @@ -0,0 +1,122 @@ +import PrefSelect from "@/components/pref/Select" +import { + availableLanguageListAtom, + clangdVersionAtom, + defaultLanguageAtom, + enableCxxAtom, + enablePythonAtom, + gccVersionAtom, + hostnameAtom, + pyrightsVersionAtom, + pythonVersionAtom, + setupDeviceAtom, +} from "@/store/setting/setup" +import { useAtomValue, useSetAtom } from "jotai" +import { useState } from "react" +import { useNavigate } from "react-router-dom" +import Logo from "./logo" +import SetupCXX from "../../components/setup/setup-cxx" +import SetupPy from "../../components/setup/setup-py" +import { Button } from "@/components/ui/button" +import useReadAtom from "@/hooks/useReadAtom" +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" + +export default function Setup() { + const hostname = useAtomValue(hostnameAtom) + const setSetupHostname = useSetAtom(setupDeviceAtom) + const navgiate = useNavigate() + const availableLanguageList = useAtomValue(availableLanguageListAtom) + const [alertOpen, setAlertOpen] = useState(false) + const [alertMessage, setAlertMessage] = useState("") + const readDefaultLang = useReadAtom(defaultLanguageAtom) + const cxxCfg = { + readEnable: useReadAtom(enableCxxAtom), + readGCCV: useReadAtom(gccVersionAtom), + readClangdV: useReadAtom(clangdVersionAtom), + } + const pythonCfg = { + readEnable: useReadAtom(enablePythonAtom), + readPythonV: useReadAtom(pythonVersionAtom), + readPyrightsV: useReadAtom(pyrightsVersionAtom), + } + + async function done() { + const defaultLang = await readDefaultLang() + if (defaultLang == null) { + setAlertMessage("The default language must be setted") + setAlertOpen(true) + return + } + if ((await cxxCfg.readEnable()) && (await cxxCfg.readGCCV()) == null) { + setAlertMessage("C++ Compiler Path must be setted for compiling c++ program") + setAlertOpen(true) + return + } + if ((await cxxCfg.readEnable()) && (await cxxCfg.readClangdV()) == null) { + setAlertMessage("Clangd Path must be setted for providing code auto-completation and lints") + setAlertOpen(true) + return + } + if ((await pythonCfg.readEnable()) && (await pythonCfg.readPythonV()) == null) { + setAlertMessage("Python Interpreter Path must be setted for run python program") + setAlertOpen(true) + return + } + if ((await pythonCfg.readEnable()) && (await pythonCfg.readPyrightsV()) == null) { + setAlertMessage("Pyrights Path must be setted for providing code auto-completation and lints") + setAlertOpen(true) + return + } + setSetupHostname(hostname) + navgiate("/") + } + + return ( +
    + + + + Error + {alertMessage} + + + Ok + + + +
    + +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • +
      + +
      +
    • +
    +
    +
    + ) +} diff --git a/src/pages/Setup/logo.tsx b/src/pages/Setup/logo.tsx new file mode 100644 index 0000000..9192744 --- /dev/null +++ b/src/pages/Setup/logo.tsx @@ -0,0 +1,21 @@ +import { motion } from "framer-motion" +import LogoImage from "../../../src-tauri/icons/128x128@2x.png" +export default function Logo() { + return ( + + +
    +

    Provlegisto

    + The simple IDE specially designed for ACMer/OIer +
    +
    + ) +} diff --git a/src/router.tsx b/src/router.tsx index da0828d..5055432 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -8,10 +8,17 @@ const PagePreference = { Appearance: lazy(() => import("@/pages/Preference/appearance")), Editor: lazy(() => import("@/pages/Preference/editor")), Keymap: lazy(() => import("@/pages/Preference/keymap")), + Language: lazy(() => import("@/pages/Preference/language")), } +const PageSetup = lazy(() => import("@/pages/Setup")) +const PageInstall = lazy(() => import("@/pages/Install")) const PageAbout = lazy(() => import("@/pages/About")) const router = createBrowserRouter([ + { + path: "/setup", + element: , + }, { path: "/", element: , @@ -21,7 +28,7 @@ const router = createBrowserRouter([ element: , children: [ { - path: "/pref/appearance", + path: "/pref", element: , }, { @@ -32,12 +39,24 @@ const router = createBrowserRouter([ path: "/pref/keymap", element: , }, + { + path: "/pref/lang", + element: , + }, ], }, { path: "/about", element: , }, + { + path: "/setup", + element: , + }, + { + path: "/install/:name", + element: , + }, ]) export default function Router() { diff --git a/src/store/setting/index.ts b/src/store/setting/index.ts index f5e0337..1fdb9c8 100644 --- a/src/store/setting/index.ts +++ b/src/store/setting/index.ts @@ -1,4 +1,4 @@ -export * from "./ui" +import * as log from "tauri-plugin-log-api" import { Store } from "tauri-plugin-store-api" import { atomWithStorage } from "jotai/utils" import { getSettingsPath } from "@/lib/ipc" @@ -7,31 +7,39 @@ let store: Store | null = null export async function loadSettingsStore() { const path = await getSettingsPath() store = new Store(path) + return } export function atomWithSettings(key: string, initialValue: T) { - const atom = atomWithStorage(key, initialValue, { - async getItem(key, initialValue) { - if (store == null) throw new Error("Store not init") - return (await store.get(key)) ?? initialValue + const atom = atomWithStorage( + key, + initialValue, + { + async getItem(key, initialValue) { + if (store == null) throw new Error("Store not init") + return (await store.get(key)) ?? initialValue + }, + async setItem(key, newValue) { + if (store == null) throw new Error("Store not init") + await store!.set(key, newValue) + }, + async removeItem(key) { + if (store == null) throw new Error("Store not init") + await store.delete(key) + }, + subscribe(key, callback, initialValue) { + const unlisten = store!.onChange((k, value) => { + if (key == k && value != initialValue) callback(value as T) + }) + return () => { + unlisten.then((fn) => fn()).catch((reson) => log.error(reson)) + } + }, }, - async setItem(key, newValue) { - if (store == null) throw new Error("Store not init") - await store!.set(key, newValue) + { + unstable_getOnInit: true, }, - async removeItem(key) { - if (store == null) throw new Error("Store not init") - await store.delete(key) - }, - subscribe(key, callback, initialValue) { - const unlisten = store!.onChange((k, value) => { - if (key == k && value != initialValue) callback(value as T) - }) - return () => { - unlisten.then((fn) => fn()) - } - }, - }) + ) atom.debugLabel = `settings.${key}` return atom -} +} \ No newline at end of file diff --git a/src/store/setting/setup.ts b/src/store/setting/setup.ts new file mode 100644 index 0000000..fc91610 --- /dev/null +++ b/src/store/setting/setup.ts @@ -0,0 +1,93 @@ +import { atom } from "jotai" +import { atomWithSettings } from "." +import { getHostname } from "@/lib/ipc/host" +import { LanguageMode } from "@/lib/ipc" +import { captureOutput } from "@/lib/ipc/setup" +import * as log from "tauri-plugin-log-api" +import { PrefSelectItem } from "@/components/pref/Select" +import { loadable } from "jotai/utils" + +export const hostnameAtom = atom(() => getHostname()) +export const setupDeviceAtom = atomWithSettings("setup", "") + +const internalDefaultLanguageAtom = atomWithSettings("setup.language", LanguageMode.CXX) +export const defaultLanguageAtom = atom(async (get)=> { + const lang = await get(internalDefaultLanguageAtom) + const cxxEnable = await get(enableCxxAtom) + const pyEnable = await get(enablePythonAtom) + + if(lang == LanguageMode.CXX && cxxEnable) return LanguageMode.CXX + if(lang == LanguageMode.PY && pyEnable) return LanguageMode.PY + return null +}, (_get, set, value: LanguageMode)=>{ + set(internalDefaultLanguageAtom, value) +}) + +defaultLanguageAtom.debugLabel = "settings.setup.language.export" + +export const enableCxxAtom = atomWithSettings("setup.cxx", true) +export const gccPathAtom = atomWithSettings("gcc.path", "g++") + +export const gccVersionAtom = atom(async (get) => { + const path = await get(gccPathAtom) + if (path.length == 0) return null + try { + const output = await captureOutput(path, ["--version"]) + const version = output.stdout.split("\n")[0] + return version + } catch (e) { + log.error(JSON.stringify(e)) + return null + } +}) + +export const clangdPathAtom = atomWithSettings("clangd.path", "clangd") +export const clangdVersionAtom = atom(async (get) => { + const path = await get(clangdPathAtom) + if (path.length == 0) return null + try { + const output = await captureOutput(path, ["--version"]) + return output.stdout + } catch (e) { + log.error(JSON.stringify(e)) + return null + } +}) + +export const enablePythonAtom = atomWithSettings("setup.python", false) +export const pythonPathAtom = atomWithSettings("python.path", "python") +export const pythonVersionAtom = atom(async (get) => { + const path = await get(pythonPathAtom) + if (path.length == 0) return null + try { + const output = await captureOutput(path, ["--version"]) + return output.stdout + } catch (e) { + log.error(JSON.stringify(e)) + return null + } +}) + +export const pyrightsPathAtom = atomWithSettings("pyrights.path", "pyright-langserver") +export const pyrightsVersionAtom = atom(async (get) => { + const path = await get(pyrightsPathAtom) + if (path.length == 0) return null + try { + await captureOutput(path, ["--version"]) + return "Pyrights installed" + } catch (e) { + log.error(JSON.stringify(e)) + return null + } +}) + +export const availableLanguageListAtom = loadable( + atom(async (get) => { + const enableCXX = await get(enableCxxAtom) + const enablePY = await get(enablePythonAtom) + let list: PrefSelectItem[] = [] + if (enableCXX) list.push({ key: LanguageMode.CXX, value: "C++" }) + if (enablePY) list.push({ key: LanguageMode.PY, value: "Python" }) + return list + }), +) diff --git a/src/store/source.ts b/src/store/source.ts index bb9b6b3..87f9d6a 100644 --- a/src/store/source.ts +++ b/src/store/source.ts @@ -17,10 +17,10 @@ export type Source = { test: Test } -export function emptySource(): Source { +export function emptySource(language: LanguageMode): Source { return { code: { - language: LanguageMode.CXX, + language, source: "", }, test: { @@ -44,16 +44,19 @@ type SourceStore = { const activeIdInternalAtom = atom(-1) activeIdInternalAtom.debugLabel = "source.active.internal" -export const activeIdAtom = atom((get)=>{ - let headers = get(sourceIndexAtoms) - if(headers.length == 0) return -1 - let id = get(activeIdInternalAtom) - if(id == -1 && headers.length != 0) return headers[0].id - if(id != -1 && headers.findIndex((p)=> p.id == id) == -1) return -1 - return id -}, (_get, set, value:number)=>{ - set(activeIdInternalAtom, value) -}) +export const activeIdAtom = atom( + (get) => { + let headers = get(sourceIndexAtoms) + if (headers.length == 0) return -1 + let id = get(activeIdInternalAtom) + if (id == -1 && headers.length != 0) return headers[0].id + if (id != -1 && headers.findIndex((p) => p.id == id) == -1) return -1 + return id + }, + (_get, set, value: number) => { + set(activeIdInternalAtom, value) + }, +) activeIdAtom.debugLabel = "source.active" export const counterAtom = atomWithReducer(0, (prev, value: number | undefined) => { diff --git a/src/store/system.ts b/src/store/system.ts new file mode 100644 index 0000000..c123c0a --- /dev/null +++ b/src/store/system.ts @@ -0,0 +1,6 @@ +import { getSystemName } from "@/lib/ipc/host" +import { atom } from "jotai" + +export const systemNameAtom = atom(async () => { + return getSystemName() +})