diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..197185b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: Publish docs via GitHub Pages + +on: + push: + pull_request: + types: [closed] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [ 18.x ] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install pageforge -g + + - name: Build + run: | + cd docs + pageforge build + echo 'codeforge.devlive.org' > dist/CNAME + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GH_TOKEN }} + publish_dir: docs/dist \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e9a2bcf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,144 @@ +name: Release + +on: + # 新发布触发 + release: + types: [published] + + # 手动触发 + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g., v1.0.0)' + required: true + type: string + +env: + CARGO_TERM_COLOR: always + NODE_VERSION: '18' + RUST_VERSION: '1.85.0' + PNPM_VERSION: '8' + +jobs: + # 构建发布版本 + build-release: + name: Build Release + runs-on: ${{ matrix.platform }} + + strategy: + fail-fast: false + matrix: + platform: [macos-latest, windows-latest] + include: + - platform: macos-latest + os: macos + target: universal-apple-darwin + - platform: windows-latest + os: windows + target: x86_64-pc-windows-msvc + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Add Rust targets (macOS) + if: matrix.platform == 'macos-latest' + run: | + rustup target add aarch64-apple-darwin + rustup target add x86_64-apple-darwin + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install frontend dependencies + run: | + if [ -f "pnpm-lock.yaml" ]; then + pnpm install --frozen-lockfile + else + pnpm install + fi + shell: bash + + - name: Build application + run: pnpm tauri build --target ${{ matrix.target }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.os }} + path: src-tauri/target/release/bundle/ + retention-days: 30 + + # 创建或更新 GitHub Release + create-release: + name: Create Release + needs: build-release + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Prepare release assets + run: | + mkdir -p release-assets + find artifacts -name "*.dmg" -exec cp {} release-assets/ \; + find artifacts -name "*.msi" -exec cp {} release-assets/ \; + find artifacts -name "*.exe" -exec cp {} release-assets/ \; + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.event.inputs.tag }} + name: Release ${{ github.event.inputs.tag }} + files: release-assets/* + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + # 更新现有 Release + update-release: + name: Update Release + needs: build-release + runs-on: ubuntu-latest + if: github.event_name == 'release' + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Prepare release assets + run: | + mkdir -p release-assets + find artifacts -name "*.dmg" -exec cp {} release-assets/ \; + find artifacts -name "*.msi" -exec cp {} release-assets/ \; + find artifacts -name "*.exe" -exec cp {} release-assets/ \; + + - name: Upload to existing release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.event.release.tag_name }} + files: release-assets/* + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e7b8986..1b41949 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ dist-ssr pnpm-lock.yaml *.csv *.json +!config*.json diff --git a/README.md b/README.md index aee4c81..9ce9621 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,6 @@ pnpm tauri dev pnpm tauri build ``` -## 使用 - -1. 在左侧编辑器输入代码 -2. 点击 "执行代码" 按钮执行 -3. 在右侧面板查看输出结果 - ## 技术栈 - **前端:** Vue 3 + TypeScript + Tailwind CSS diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..faa0113 --- /dev/null +++ b/config/config.json @@ -0,0 +1,18 @@ +{ + "log_directory": null, + "auto_clear_logs": true, + "keep_log_days": 30, + "theme": "system", + "plugins": [ + { + "enabled": true, + "execute_home": null, + "extensions": [], + "language": "python2", + "before_compile": null, + "after_compile": null, + "run_command": null, + "template": null + } + ] +} \ No newline at end of file diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..31b21bd --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +codeforge.devlive.org \ No newline at end of file diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..31552aa --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + < + > + + + + + + + + + + + + + + + + + + + + + + + + + { } + + + + + 010110 + + + 110101 + + + 101011 + + + 011010 + + + + + CodeForge + + \ No newline at end of file diff --git a/docs/content/index.md b/docs/content/index.md new file mode 100644 index 0000000..3261d8d --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,64 @@ +--- +title: 欢迎使用 CodeForge +template: home + +config: + sidebar: false + toc: false + +hero: + title: 轻量级桌面代码执行器 + description: 轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。 + primaryCta: + url: /download.html + text: 立即下载 + secondaryCta: + url: /<%= pageData.language %>/usage/href.html + text: 了解更多 + +features: + subtitle: 核心优势 + title: 为什么选择我们 + description: 轻量级、高性能,专为开发者、学生和编程爱好者设计。 + items: + - icon: >- + + + + title: 高性能 + description: 可扩展的语言支持系统 + + - icon: >- + + + + title: 操作简洁 + description: 一键运行代码 + + - icon: >- + + + + title: 易于使用 + description: 直观的界面设计,简单易上手 + +stats: + title: 用数据说话 + description: 我们取得的成就 + items: + - label: GitHub Stars + value: 0+ + - label: Gitee Stars + value: 0+ + - label: 正常运行时间 + value: 99.99% + - label: 客户满意度 + value: 0% + +cta: + title: 准备好开始了吗? + description: 立即下载,开启您的技术创新之旅 + button: + url: /download.html + text: 立即下载 +--- \ No newline at end of file diff --git a/docs/pageforge.yaml b/docs/pageforge.yaml new file mode 100644 index 0000000..36fdd82 --- /dev/null +++ b/docs/pageforge.yaml @@ -0,0 +1,21 @@ +site: + title: CodeForge + description: CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。 + hiddenTitle: true + keywords: CodeForge, Markdown, Python 2, Python 3, Rust + logo: /assets/logo.svg + favicon: assets/logo.svg + baseUrl: https://codeforge.devlive.org + +repo: + owner: devlive-community + name: codeforge + url: https://github.com/devlive-community/codeforge + branch: dev + +footer: + copyright: © 2025 CodeForge All Rights Reserved. + social: + github: + title: GitHub + href: https://github.com/devlive-community/codeforge diff --git a/package.json b/package.json index dd4c7ac..1391fe1 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,18 @@ }, "dependencies": { "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.3.2", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-shell": "^2.3.0", "@vueuse/core": "^13.6.0", + "lodash-es": "^4.17.21", "lucide-vue-next": "^0.539.0", "vue": "^3.5.13" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", "@tauri-apps/cli": "^2", + "@types/lodash-es": "^4.17.12", "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.21", "postcss": "^8.5.6", diff --git a/public/icons/python.svg b/public/icons/python.svg new file mode 100644 index 0000000..37b7428 --- /dev/null +++ b/public/icons/python.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a358b2f..79a0fbe 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,11 +7,16 @@ name = "CodeForge" version = "25.0.0" dependencies = [ "chrono", + "dirs", + "fern", + "log", "serde", "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-opener", + "tauri-plugin-shell", "tempfile", "tokio", "uuid", @@ -77,6 +82,27 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -760,6 +786,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.1", + "block2 0.6.1", + "libc", "objc2 0.6.1", ] @@ -774,6 +802,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.7.0" @@ -797,6 +834,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -853,6 +896,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -942,6 +994,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -2399,6 +2460,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "pango" version = "0.18.3" @@ -2636,7 +2707,7 @@ checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", "indexmap 2.10.0", - "quick-xml", + "quick-xml 0.38.1", "serde", "time", ] @@ -2765,6 +2836,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.1" @@ -2814,6 +2894,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2834,6 +2924,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2852,6 +2952,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -2980,6 +3089,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -3080,6 +3214,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3283,12 +3423,44 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -3681,6 +3853,46 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e5858cc7b455a73ab4ea2ebc08b5be33682c00ff1bf4cad5537d4fb62499d9" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "toml 0.8.23", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.4.0" @@ -3703,6 +3915,27 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-shell" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "tokio", +] + [[package]] name = "tauri-runtime" version = "2.7.1" @@ -3917,8 +4150,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "slab", "socket2", + "tracing", "windows-sys 0.59.0", ] @@ -4421,6 +4656,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.1", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -5046,6 +5341,7 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", @@ -5165,6 +5461,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 0.7.12", "zvariant_derive", "zvariant_utils", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2f711e7..7cfd137 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -11,13 +11,19 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } +chrono = { version = "0.4.41", features = ["serde"] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" +tauri-plugin-shell = "2.0" +tauri-plugin-dialog = "2.0" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = "1.47.1" uuid = { version = "1.17.0", features = ["v4"] } tempfile = "3.0" chrono = { version = "0.4", features = ["serde"] } +log = "0.4" +fern = "0.7.1" +dirs = "6.0.0" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e..db3af91 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,13 @@ fn main() { + // 设置构建时间 + let utc_time = chrono::Utc::now(); + let beijing_time = utc_time + chrono::Duration::hours(8); + let build_time = beijing_time.format("%Y年%m月%d日 %H:%M:%S").to_string(); + println!("cargo:rustc-env=BUILD_TIME={}", build_time); + + // 重新构建触发条件 + println!("cargo:rerun-if-changed=build.rs"); + + // Tauri 的构建脚本 tauri_build::build() } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49..19d0cce 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,9 +2,22 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", - "opener:default" + "opener:default", + { + "identifier": "opener:allow-open-path", + "allow": [ + { + "path": "**" + } + ] + }, + "shell:default", + "shell:allow-open", + "dialog:default" ] } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index df0a529..6cd16da 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index c2eedec..30b770b 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 3740d58..73625e8 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index d113d49..c8fa875 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index 92d275c..fec6d4b 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs new file mode 100644 index 0000000..876ec73 --- /dev/null +++ b/src-tauri/src/config.rs @@ -0,0 +1,209 @@ +use crate::plugins::PluginConfig; +// 全局配置管理器 +use crate::plugin::PluginManagerState; +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; +use tauri::{AppHandle, Manager, command}; + +static CONFIG_MANAGER: Mutex> = Mutex::new(None); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppConfig { + pub log_directory: Option, + pub auto_clear_logs: Option, + pub keep_log_days: Option, + pub theme: Option, + pub plugins: Option>, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + log_directory: None, + auto_clear_logs: Some(true), + keep_log_days: Some(30), + theme: Some("system".to_string()), + plugins: Some(vec![]), + } + } +} + +pub struct ConfigManager { + config_path: PathBuf, + config: AppConfig, +} + +impl ConfigManager { + pub fn new(app_handle: Option<&AppHandle>) -> Result { + let config_path = Self::get_config_path()?; + let config = Self::load_config(&config_path, app_handle)?; + + Ok(Self { + config_path, + config, + }) + } + + fn get_config_path() -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| "无法获取用户主目录".to_string())?; + + let config_dir = home_dir.join(".codeforge"); + let config_file = config_dir.join("config.json"); + + // 确保配置目录存在 + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(|e| format!("创建配置目录失败: {}", e))?; + } + + Ok(config_file) + } + + fn load_config( + config_path: &PathBuf, + app_handle: Option<&AppHandle>, + ) -> Result { + println!("读取配置 -> 正在读取配置文件 {:?}", config_path); + if config_path.exists() { + match fs::read_to_string(config_path) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(mut config) => { + println!("读取配置 -> 成功加载配置文件: {:?}", config_path); + + // 检查 plugins 是否为 null,如果是则加载默认配置 + if config.plugins.is_none() { + println!("读取配置 -> plugins 为 null,加载默认插件配置"); + config.plugins = Self::get_default_plugins_config(app_handle); + } + + Ok(config) + } + Err(e) => { + warn!("读取配置 -> 配置文件格式错误,使用默认配置: {}", e); + Ok(Self::create_default_config(app_handle)) + } + }, + Err(e) => { + warn!("读取配置 -> 读取配置文件失败,使用默认配置: {}", e); + Ok(Self::create_default_config(app_handle)) + } + } + } else { + println!("读取配置 -> 配置文件不存在,使用默认配置"); + Ok(Self::create_default_config(app_handle)) + } + } + + fn get_default_plugins_config(app_handle: Option<&AppHandle>) -> Option> { + if let Some(handle) = app_handle { + // 从 Tauri 状态中获取 PluginManager + if let Some(plugin_manager_state) = handle.try_state::() { + // 同步访问插件管理器 + if let Ok(manager) = plugin_manager_state.try_lock() { + return Some(manager.get_all_plugin_default_config()); + } else { + println!("读取配置 -> 无法获取插件管理器锁,使用空配置"); + } + } else { + println!("读取配置 -> 无法获取插件管理器状态,使用空配置"); + } + } + Some(vec![]) + } + + fn create_default_config(app_handle: Option<&AppHandle>) -> AppConfig { + AppConfig { + log_directory: None, + auto_clear_logs: Some(true), + keep_log_days: Some(30), + theme: Some("system".to_string()), + plugins: Self::get_default_plugins_config(app_handle), + } + } + + pub fn save_config(&self) -> Result<(), String> { + let content = serde_json::to_string_pretty(&self.config) + .map_err(|e| format!("序列化配置失败: {}", e))?; + + fs::write(&self.config_path, content).map_err(|e| format!("写入配置文件失败: {}", e))?; + + info!("保存配置 -> 配置文件已保存 {}", self.config_path.display()); + Ok(()) + } + + pub fn get_config(&self) -> &AppConfig { + &self.config + } + + pub fn get_log_directory(&self) -> Option<&str> { + self.config.log_directory.as_deref() + } + + pub fn set_log_directory(&mut self, path: Option) -> Result<(), String> { + self.config.log_directory = path; + self.save_config() + } +} + +// 初始化配置 +pub fn init_config(app_handle: Option<&AppHandle>) -> Result<(), String> { + let config_manager = ConfigManager::new(app_handle)?; + + // 如果配置中有自定义日志目录,设置到日志系统 + if let Some(log_dir) = config_manager.get_log_directory() { + println!("读取配置 -> 从配置文件加载日志目录: {}", log_dir); + // 使用内部函数设置,避免循环保存 + if let Err(e) = crate::logger::set_log_directory_internal(log_dir.to_string()) { + warn!("读取配置 -> 应用配置中的日志目录失败: {}", e); + } + } + + let mut guard = CONFIG_MANAGER.lock().unwrap(); + *guard = Some(config_manager); + + Ok(()) +} + +pub fn get_config_manager() -> Result>, String> +{ + CONFIG_MANAGER + .lock() + .map_err(|e| format!("获取配置管理器失败: {}", e)) +} + +#[command] +pub async fn get_app_config() -> Result { + let guard = get_config_manager()?; + if let Some(config_manager) = guard.as_ref() { + Ok(config_manager.get_config().clone()) + } else { + Err("配置管理器未初始化".to_string()) + } +} + +pub fn get_app_config_internal() -> Result { + let guard = get_config_manager()?; + if let Some(config_manager) = guard.as_ref() { + Ok(config_manager.get_config().clone()) + } else { + Err("配置管理器未初始化".to_string()) + } +} + +#[command] +pub async fn update_app_config(config: AppConfig) -> Result<(), String> { + let mut guard = get_config_manager()?; + if let Some(config_manager) = guard.as_mut() { + config_manager.config = config; + config_manager.save_config() + } else { + Err("配置管理器未初始化".to_string()) + } +} + +#[command] +pub async fn get_config_path() -> Result { + ConfigManager::get_config_path().map(|path| path.to_string_lossy().to_string()) +} diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs new file mode 100644 index 0000000..831445a --- /dev/null +++ b/src-tauri/src/execution.rs @@ -0,0 +1,409 @@ +use crate::plugins::{CodeExecutionRequest, ExecutionResult, PluginManager}; +use log::{error, info, warn}; +use std::collections::HashMap; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; +use std::sync::{Arc, OnceLock, mpsc}; +use std::thread; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::{AppHandle, Emitter, State}; +use tokio::sync::Mutex; +use uuid::Uuid; + +// 执行任务结构 +#[derive(Debug)] +pub struct ExecutionTask { + #[allow(dead_code)] + pub language: String, + #[allow(dead_code)] + pub process_id: u32, + pub stop_flag: Arc>, +} + +pub type ExecutionHistory = Mutex>; +pub type PluginManagerState = Mutex; + +// 全局任务管理器 +type TaskManager = Arc>>; +static TASK_MANAGER: OnceLock = OnceLock::new(); + +// 初始化任务管理器 +fn init_task_manager() -> TaskManager { + TASK_MANAGER + .get_or_init(|| Arc::new(Mutex::new(HashMap::new()))) + .clone() +} + +// 停止执行命令 +#[tauri::command] +pub async fn stop_execution(language: String) -> Result { + let task_manager = init_task_manager(); + let mut guard = task_manager.lock().await; + + if let Some(task) = guard.remove(&language) { + // 设置停止标志 + { + let mut stop_flag = task.stop_flag.lock().await; + *stop_flag = true; + } + info!("停止执行 -> 成功设置停止标志给语言 [ {} ]", language); + Ok(true) + } else { + warn!("停止执行 -> 语言 [ {} ] 没有正在运行的任务", language); + Ok(false) + } +} + +// 检查是否有正在运行的任务 +#[tauri::command] +pub async fn is_execution_running(language: String) -> Result { + let task_manager = init_task_manager(); + let guard = task_manager.lock().await; + Ok(guard.contains_key(&language)) +} + +// 通用的代码执行函数 +#[tauri::command] +pub async fn execute_code( + request: CodeExecutionRequest, + history: State<'_, ExecutionHistory>, + plugin_manager: State<'_, PluginManagerState>, + app: AppHandle, +) -> Result { + info!("执行代码 -> 调用插件 [ {} ] 开始", request.language); + + // 先停止之前可能正在运行的任务 + let _ = stop_execution(request.language.clone()).await; + + let manager = plugin_manager.lock().await; + let plugin = manager + .get_plugin(&request.language) + .ok_or_else(|| format!("Unsupported language: {}", request.language))?; + + let execution_id = Uuid::new_v4().to_string(); + let temp_dir = std::env::temp_dir(); + let file_path = temp_dir.join(format!( + "codeforge_{}_{}.{}", + request.language, + execution_id, + plugin.get_file_extension() + )); + + let processed_code = plugin.pre_execute_hook(&request.code).map_err(|e| { + error!( + "执行代码 -> 调用插件 [ {} ] pre_execute_hook 出现错误 {:?}", + request.language, e + ); + format!("Pre-execution hook failed: {}", e) + })?; + + // 写入代码到临时文件 + fs::write(&file_path, &processed_code) + .map_err(|e| format!("Failed to write temporary file: {}", e))?; + + let start_time = std::time::Instant::now(); + + let cmd = plugin.get_command(None); + let args = plugin.get_execute_args(file_path.to_str().unwrap()); + info!( + "执行代码 -> 调用插件 [ {} ] 执行命令 {} 携带参数 {}", + request.language, + cmd, + args.join(" ") + ); + + // 发送执行开始事件 + let _ = app.emit( + "code-execution-start", + serde_json::json!({ + "language": request.language + }), + ); + + // 启动子进程 + let mut child = match Command::new(&cmd) + .args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(e) => { + let execution_time = start_time.elapsed().as_millis(); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let _ = fs::remove_file(&file_path); + let _ = app.emit( + "code-execution-complete", + serde_json::json!({ + "language": request.language, + "success": false + }), + ); + + error!("执行代码 -> 调用插件 [ {} ] 失败: {}", request.language, e); + return Ok(ExecutionResult { + success: false, + stdout: String::new(), + stderr: format!( + "{} interpreter not found. Please install {} and ensure it's in your PATH.\n\nError: {}", + request.language, request.language, e + ), + execution_time, + timestamp, + language: request.language, + }); + } + }; + + // 创建停止标志 + let stop_flag = Arc::new(tokio::sync::Mutex::new(false)); + + // 将任务添加到管理器 + let task_manager = init_task_manager(); + { + let mut guard = task_manager.lock().await; + guard.insert( + request.language.clone(), + ExecutionTask { + language: request.language.clone(), + process_id: child.id(), + stop_flag: stop_flag.clone(), + }, + ); + } + + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + let (stdout_tx, stdout_rx) = mpsc::channel::(); + let (stderr_tx, stderr_rx) = mpsc::channel::(); + + // 读取 stdout + thread::spawn(move || { + let reader = BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + if stdout_tx.send(line).is_err() { + break; + } + } + }); + + // 读取 stderr + thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + if stderr_tx.send(line).is_err() { + break; + } + } + }); + + let mut stdout_lines = Vec::new(); + let mut stderr_lines = Vec::new(); + let timeout = std::time::Duration::from_secs(plugin.get_timeout()); + + // 主执行循环 + loop { + // 检查停止标志 + { + let stop_guard = stop_flag.lock().await; + if *stop_guard { + info!( + "执行代码 -> 收到停止信号,终止语言 [ {} ] 的执行", + request.language + ); + let _ = child.kill(); + let _ = child.wait(); + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let _ = app.emit( + "code-execution-stopped", + serde_json::json!({ + "language": request.language + }), + ); + + return Err("代码执行被用户停止".to_string()); + } + } + + // 检查超时 + if start_time.elapsed() > timeout { + let _ = child.kill(); + let _ = child.wait(); + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let _ = app.emit( + "code-execution-timeout", + serde_json::json!({ + "language": request.language + }), + ); + + error!( + "执行代码 -> 超时 ({} 秒),终止语言 [ {} ] 的执行", + plugin.get_timeout(), + request.language + ); + return Err(format!("代码执行超时({} 秒)", plugin.get_timeout())); + } + + // 读取并发送 stdout + while let Ok(line) = stdout_rx.try_recv() { + stdout_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stdout", + "content": line, + "language": request.language + }), + ); + } + + // 读取并发送 stderr + while let Ok(line) = stderr_rx.try_recv() { + stderr_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stderr", + "content": line, + "language": request.language + }), + ); + } + + // 检查进程是否结束 + match child.try_wait() { + Ok(Some(status)) => { + // 进程已结束,读取剩余输出 + while let Ok(line) = stdout_rx.try_recv() { + stdout_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stdout", + "content": line, + "language": request.language + }), + ); + } + while let Ok(line) = stderr_rx.try_recv() { + stderr_lines.push(line.clone()); + let _ = app.emit( + "code-output", + serde_json::json!({ + "type": "stderr", + "content": line, + "language": request.language + }), + ); + } + + let execution_time = start_time.elapsed().as_millis(); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let mut result = ExecutionResult { + success: status.success(), + stdout: stdout_lines.join("\n"), + stderr: stderr_lines.join("\n"), + execution_time, + timestamp, + language: request.language.clone(), + }; + + let _ = plugin.post_execute_hook(&mut result); + + let _ = app.emit( + "code-execution-complete", + serde_json::json!({ + "language": request.language, + "success": result.success, + "execution_time": result.execution_time + }), + ); + + drop(manager); + let mut history_guard = history.lock().await; + history_guard.push(result.clone()); + + if history_guard.len() > 100 { + history_guard.remove(0); + } + + info!("执行代码 -> 调用插件 [ {} ] 完成", request.language); + return Ok(result); + } + Ok(None) => { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + Err(e) => { + let _ = child.kill(); + let _ = child.wait(); + let _ = fs::remove_file(&file_path); + + // 从任务管理器中移除 + { + let mut guard = task_manager.lock().await; + guard.remove(&request.language); + } + + let _ = app.emit( + "code-execution-error", + serde_json::json!({ + "language": request.language, + "error": e.to_string() + }), + ); + + return Err(format!("检查进程状态失败: {}", e)); + } + } + } +} + +// 获取执行历史 +#[tauri::command] +pub async fn get_execution_history( + history: State<'_, ExecutionHistory>, +) -> Result, String> { + let history_guard = history.lock().await; + Ok(history_guard.clone()) +} + +// 清空执行历史 +#[tauri::command] +pub async fn clear_execution_history(history: State<'_, ExecutionHistory>) -> Result<(), String> { + let mut history_guard = history.lock().await; + history_guard.clear(); + Ok(()) +} diff --git a/src-tauri/src/logger.rs b/src-tauri/src/logger.rs new file mode 100644 index 0000000..c466c83 --- /dev/null +++ b/src-tauri/src/logger.rs @@ -0,0 +1,153 @@ +use chrono::Local; +use log::{LevelFilter, info, warn}; +use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; +use tauri::{AppHandle, Manager}; + +static LOG_DIRECTORY: Mutex> = Mutex::new(None); + +pub fn setup_logger(app: &AppHandle) -> Result<(), fern::InitError> { + // 获取日志目录 + let log_dir = get_effective_log_directory(app); + + // 创建日志目录 + if let Err(e) = fs::create_dir_all(&log_dir) { + eprintln!("Failed to create log directory: {}", e); + let default_dir = get_default_log_directory(app); + fs::create_dir_all(&default_dir).expect("Failed to create default log directory"); + + { + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = None; + } + + warn!("日志目录创建失败,使用默认目录: {:?}", default_dir); + } + + // 生成当天的日志文件名 + let today = Local::now().format("%Y-%m-%d").to_string(); + + // 不同级别的日志文件 + let all_log_file = log_dir.join(format!("codeforge-{}.log", today)); + let error_log_file = log_dir.join(format!("codeforge-error-{}.log", today)); + let warn_log_file = log_dir.join(format!("codeforge-warn-{}.log", today)); + let info_log_file = log_dir.join(format!("codeforge-info-{}.log", today)); + let debug_log_file = log_dir.join(format!("codeforge-debug-{}.log", today)); + + // 基础配置 + let base_config = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}] [{}] [{}:{}] {}", + Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), + record.level(), + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + message + )) + }) + .level(LevelFilter::Debug) + .level_for("hyper", LevelFilter::Warn) + .level_for("reqwest", LevelFilter::Warn) + .level_for("tauri", LevelFilter::Info); + + // 配置不同级别的日志输出 + base_config + .chain(std::io::stdout()) // 控制台输出 + .chain(fern::log_file(&all_log_file)?) // 所有级别写入总文件 + .chain( + fern::Dispatch::new() + .filter(|metadata| metadata.level() == log::Level::Error) + .chain(fern::log_file(&error_log_file)?), + ) + .chain( + fern::Dispatch::new() + .filter(|metadata| metadata.level() == log::Level::Warn) + .chain(fern::log_file(&warn_log_file)?), + ) + .chain( + fern::Dispatch::new() + .filter(|metadata| metadata.level() == log::Level::Info) + .chain(fern::log_file(&info_log_file)?), + ) + .chain( + fern::Dispatch::new() + .filter(|metadata| metadata.level() == log::Level::Debug) + .chain(fern::log_file(&debug_log_file)?), + ) + .apply()?; + + info!("CodeForge 应用启动"); + info!("应用版本: {}", env!("CARGO_PKG_VERSION")); + info!("日志文件目录 {}", log_dir.display()); + + Ok(()) +} + +// 获取有效的日志目录(考虑自定义设置) +fn get_effective_log_directory(app: &AppHandle) -> PathBuf { + // 首先检查是否有自定义日志目录 + let custom_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + guard.clone() + }; + + match custom_dir { + Some(dir) => { + info!("使用自定义日志目录: {:?}", dir); + dir + } + None => { + let default_dir = get_default_log_directory(app); + info!("使用默认日志目录: {:?}", default_dir); + default_dir + } + } +} + +// 获取默认日志目录 +fn get_default_log_directory(app: &AppHandle) -> PathBuf { + app.path() + .app_data_dir() + .expect("Failed to get app data dir") + .join("logs") +} + +// 公共函数,供其他模块调用 +pub fn get_log_directory(app: &AppHandle) -> PathBuf { + get_effective_log_directory(app) +} + +// 内部函数,用于配置系统调用(不保存到配置文件) +pub fn set_log_directory_internal(path: String) -> Result<(), String> { + let new_path = PathBuf::from(&path); + info!("设置日志 -> 内部设置日志目录为: {}", path); + + // 验证目录是否存在,如果不存在则创建 + if !new_path.exists() { + std::fs::create_dir_all(&new_path).map_err(|e| format!("无法创建目录: {}", e))?; + } + + // 验证是否为目录 + if !new_path.is_dir() { + return Err("指定的路径不是目录".to_string()); + } + + // 更新全局日志目录 + { + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = Some(new_path); + } + + info!("设置日志 -> 日志目录已内部设置为: {}", path); + Ok(()) +} + +// 重新初始化日志系统(当日志目录改变时调用) +pub fn reinit_logger(app: &AppHandle) -> Result<(), String> { + // 重新设置日志系统 + setup_logger(app).map_err(|e| format!("重新初始化日志系统失败: {}", e))?; + info!("日志系统已重新初始化"); + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c3f178b..d3e6f92 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,225 +3,74 @@ windows_subsystem = "windows" )] +mod config; +mod execution; +mod logger; +mod plugin; mod plugins; +mod setup; +mod utils; + +use crate::execution::{ + ExecutionHistory, PluginManagerState as ExecutionPluginManagerState, clear_execution_history, + execute_code, get_execution_history, is_execution_running, stop_execution, +}; +use crate::plugin::{get_info, get_supported_languages}; +use crate::setup::app::get_app_info; +use crate::utils::logger::{ + clear_logs, get_log_directory, get_log_files, reset_log_directory, set_log_directory, +}; +use config::{get_app_config, get_config_path, init_config, update_app_config}; +use log::info; +use plugins::PluginManager; -use plugins::{CodeExecutionRequest, ExecutionResult, LanguageInfo, PluginManager}; -use std::fs; -use std::process::{Command, Stdio}; -use std::time::{SystemTime, UNIX_EPOCH}; -use tauri::State; -use tokio::sync::Mutex; -use uuid::Uuid; - -type ExecutionHistory = Mutex>; -type PluginManagerState = Mutex; - -// 通用的代码执行函数 -#[tauri::command] -async fn execute_code( - request: CodeExecutionRequest, - history: State<'_, ExecutionHistory>, - plugin_manager: State<'_, PluginManagerState>, -) -> Result { - let manager = plugin_manager.lock().await; - let plugin = manager - .get_plugin(&request.language) - .ok_or_else(|| format!("Unsupported language: {}", request.language))?; - - let execution_id = Uuid::new_v4().to_string(); - let temp_dir = std::env::temp_dir(); - let file_path = temp_dir.join(format!( - "codeforge_{}_{}.{}", - request.language, - execution_id, - plugin.get_file_extension() - )); - - // 预处理代码 - let processed_code = plugin - .pre_execute_hook(&request.code) - .map_err(|e| format!("Pre-execution hook failed: {}", e))?; - - // 写入代码到临时文件 - fs::write(&file_path, &processed_code) - .map_err(|e| format!("Failed to write temporary file: {}", e))?; - - let start_time = std::time::Instant::now(); - let mut last_error = String::new(); - - // 尝试不同的命令 - for cmd in plugin.get_commands() { - let args = plugin.get_execute_args(file_path.to_str().unwrap()); - - println!("Trying command: {} with args: {:?}", cmd, args); - - let output = Command::new(cmd) - .args(&args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output(); - - match output { - Ok(output) => { - let execution_time = start_time.elapsed().as_millis(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // 清理临时文件 - let _ = fs::remove_file(&file_path); - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - let mut result = ExecutionResult { - success: output.status.success(), - stdout, - stderr, - execution_time, - timestamp, - language: request.language.clone(), - }; - - // 后处理 - let _ = plugin.post_execute_hook(&mut result); - - // 添加到执行历史 - drop(manager); // 释放插件管理器锁 - let mut history_guard = history.lock().await; - history_guard.push(result.clone()); - - // 保持历史记录不超过100条 - if history_guard.len() > 100 { - history_guard.remove(0); - } - - return Ok(result); - } - Err(e) => { - last_error = format!("Failed to execute {} - {}", cmd, e); - continue; +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) + .manage(ExecutionHistory::default()) + .manage(ExecutionPluginManagerState::new(PluginManager::new())) + .setup(|app| { + // 第一步:初始化配置系统 + if let Err(e) = init_config(Some(app.handle())) { + eprintln!("Failed to initialize config: {}", e); } - } - } - - // 如果所有命令都失败了 - let execution_time = start_time.elapsed().as_millis(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - // 清理临时文件 - let _ = fs::remove_file(&file_path); - - Ok(ExecutionResult { - success: false, - stdout: String::new(), - stderr: format!( - "{} interpreter not found. Please install {} and ensure it's in your PATH.\n\nLast error: {}\n\nTried commands: {:?}", - request.language, - request.language, - last_error, - plugin.get_commands() - ), - execution_time, - timestamp, - language: request.language, - }) -} - -// 通用的环境信息获取函数 -#[tauri::command] -async fn get_info( - language: String, - plugin_manager: State<'_, PluginManagerState>, -) -> Result { - let manager = plugin_manager.lock().await; - let plugin = manager - .get_plugin(&language) - .ok_or_else(|| format!("Unsupported language: {}", language))?; - - // 尝试不同的命令 - for cmd in plugin.get_commands() { - println!("Trying command: {} for language: {}", cmd, language); - - let version_output = Command::new(cmd).args(plugin.get_version_args()).output(); - - if let Ok(version_out) = version_output { - if version_out.status.success() { - let path_result = Command::new(cmd) - .arg("-c") - .arg(plugin.get_path_command()) - .output(); - - let version = String::from_utf8_lossy(&version_out.stdout) - .trim() - .to_string(); - - let path = if let Ok(path_out) = path_result { - if path_out.status.success() { - String::from_utf8_lossy(&path_out.stdout).trim().to_string() - } else { - "Command found but path unavailable".to_string() - } - } else { - "Path detection failed".to_string() - }; - - return Ok(LanguageInfo { - installed: true, - version, - path, - language: plugin.get_language_name().to_string(), - }); + // 第二步:初始化日志系统 + if let Err(e) = logger::setup_logger(app.handle()) { + eprintln!("Failed to setup logger: {}", e); } - } - } - - Ok(LanguageInfo { - installed: false, - version: "Not found".to_string(), - path: format!("Not found - tried: {:?}", plugin.get_commands()), - language: plugin.get_language_name().to_string(), - }) -} -// 获取支持的语言列表 -#[tauri::command] -async fn get_supported_languages( - plugin_manager: State<'_, PluginManagerState>, -) -> Result, String> { - let manager = plugin_manager.lock().await; - Ok(manager.get_supported_languages()) -} - -#[tauri::command] -async fn get_execution_history( - history: State<'_, ExecutionHistory>, -) -> Result, String> { - let history_guard = history.lock().await; - Ok(history_guard.clone()) -} - -#[tauri::command] -async fn clear_execution_history(history: State<'_, ExecutionHistory>) -> Result<(), String> { - let mut history_guard = history.lock().await; - history_guard.clear(); - Ok(()) -} - -fn main() { - tauri::Builder::default() - .manage(ExecutionHistory::default()) - .manage(PluginManagerState::new(PluginManager::new())) + // 初始化应用菜单 + info!("初始化 -> 初始化应用菜单"); + let menu = setup::menu::create_menu(app.handle())?; + app.set_menu(menu)?; + setup::menu::setup_menu_handler(app.handle()); + Ok(()) + }) .invoke_handler(tauri::generate_handler![ + // 执行相关命令 execute_code, + stop_execution, + is_execution_running, + get_execution_history, + clear_execution_history, + // 信息相关命令 get_info, get_supported_languages, - get_execution_history, - clear_execution_history + // 应用信息命令 + get_app_info, + // 日志相关命令 + get_log_directory, + set_log_directory, + reset_log_directory, + get_log_files, + clear_logs, + // 配置相关命令 + get_app_config, + update_app_config, + get_config_path ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs new file mode 100644 index 0000000..7890409 --- /dev/null +++ b/src-tauri/src/plugin.rs @@ -0,0 +1,82 @@ +use crate::plugins::{LanguageInfo, PluginManager}; +use log::{debug, error, info}; +use std::process::Command; +use tauri::State; +use tokio::sync::Mutex; + +pub type PluginManagerState = Mutex; + +// 通用的环境信息获取函数 +#[tauri::command] +pub async fn get_info( + language: String, + plugin_manager: State<'_, PluginManagerState>, +) -> Result { + info!("获取环境 -> 调用插件 [ {} ] 开始", language); + let manager = plugin_manager.lock().await; + let plugin = manager + .get_plugin(&language) + .ok_or_else(|| format!("Unsupported language: {}", language))?; + + plugin.pre_execute_hook("").map_err(|e| { + error!( + "获取环境 -> 调用插件 [ {} ] pre_execute_hook 出现错误 {:?}", + language, e + ); + + error!("获取环境 -> 调用插件 [ {} ] 失败", language); + format!("Pre-execution hook failed: {}", e) + })?; + + let cmd = plugin.get_command(None); + debug!("获取环境 -> 插件 [ {} ] 命令 {}", language, cmd); + + let version_output = Command::new(&cmd).args(plugin.get_version_args()).output(); + if let Ok(version_out) = version_output { + if version_out.status.success() { + let path_result = Command::new(&cmd) + .arg("-c") + .arg(plugin.get_path_command()) + .output(); + + let version = String::from_utf8_lossy(&version_out.stdout) + .trim() + .to_string(); + + let path = if let Ok(path_out) = path_result { + if path_out.status.success() { + String::from_utf8_lossy(&path_out.stdout).trim().to_string() + } else { + "Command found but path unavailable".to_string() + } + } else { + "Path detection failed".to_string() + }; + + info!("获取环境 -> 调用插件 [ {} ] 完成", language); + return Ok(LanguageInfo { + installed: true, + version, + path, + language: plugin.get_language_name().to_string(), + }); + } + } + + error!("获取环境 -> 调用插件 [ {} ] 失败", language); + Ok(LanguageInfo { + installed: false, + version: "Not found".to_string(), + path: format!("Not found - tried: {:?}", plugin.get_command(None)), + language: plugin.get_language_name().to_string(), + }) +} + +// 获取支持的语言列表 +#[tauri::command] +pub async fn get_supported_languages( + plugin_manager: State<'_, PluginManagerState>, +) -> Result, String> { + let manager = plugin_manager.lock().await; + Ok(manager.get_supported_languages()) +} diff --git a/src-tauri/src/plugins/manager.rs b/src-tauri/src/plugins/manager.rs index 787641a..ad74146 100644 --- a/src-tauri/src/plugins/manager.rs +++ b/src-tauri/src/plugins/manager.rs @@ -1,4 +1,4 @@ -use super::{LanguagePlugin, python2::Python2Plugin, python3::Python3Plugin}; +use super::{LanguagePlugin, PluginConfig, python2::Python2Plugin, python3::Python3Plugin}; use std::collections::HashMap; pub struct PluginManager { @@ -53,12 +53,8 @@ impl PluginManager { pub fn get_plugin_info(&self, language: &str) -> Option { self.get_plugin(language).map(|plugin| PluginInfo { name: plugin.get_language_name().to_string(), - file_extension: plugin.get_file_extension().to_string(), - available_commands: plugin - .get_commands() - .iter() - .map(|s| s.to_string()) - .collect(), + file_extension: plugin.get_file_extension(), + available_commands: vec![plugin.get_command(None).to_string()], }) } @@ -68,15 +64,18 @@ impl PluginManager { .values() .map(|plugin| PluginInfo { name: plugin.get_language_name().to_string(), - file_extension: plugin.get_file_extension().to_string(), - available_commands: plugin - .get_commands() - .iter() - .map(|s| s.to_string()) - .collect(), + file_extension: plugin.get_file_extension(), + available_commands: vec![plugin.get_command(None).to_string()], }) .collect() } + + pub fn get_all_plugin_default_config(&self) -> Vec { + self.plugins + .values() + .map(|plugin| plugin.get_default_config()) + .collect() + } } #[derive(Debug, serde::Serialize)] diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index a0f7f4c..f003504 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -1,4 +1,7 @@ +use crate::config::get_app_config_internal; +use log::{debug, info}; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; // 通用结构定义 #[derive(Debug, Serialize, Deserialize, Clone)] @@ -25,24 +28,277 @@ pub struct LanguageInfo { pub language: String, } +// 插件配置结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginConfig { + pub enabled: bool, // 插件是否启用 + pub execute_home: Option, // 插件的执行路径 + pub extension: String, // 插件支持的文件扩展名 + pub language: String, // 插件所属语言 + pub before_compile: Option, // 插件在编译前执行的命令 + pub after_compile: Option, // 插件在编译完成后执行的命令 + pub run_command: Option, // 插件执行的命令,例如 "python2 $filename" + pub template: Option, // 插件的模板 + pub timeout: Option, // 插件的超时时间 +} + // 语言插件接口 pub trait LanguagePlugin: Send + Sync { + // 获取插件优先级 fn get_order(&self) -> i32 { 0 } + + // 获取插件名称 fn get_language_name(&self) -> &'static str; - fn get_file_extension(&self) -> &'static str; - fn get_commands(&self) -> Vec<&'static str>; + + // 获取插件唯一标记 + fn get_language_key(&self) -> &'static str; + + // 获取插件支持的文件扩展名 + fn get_file_extension(&self) -> String { + self.get_config().unwrap().extension.clone() + } + + // 获取执行目录 + fn get_execute_home(&self) -> Option { + self.get_config() + .and_then(|config| config.execute_home.clone()) + .filter(|path| !path.trim().is_empty()) // 过滤掉空字符串和只有空白字符的字符串 + .map(PathBuf::from) + } + + // 获取超时时间 + fn get_timeout(&self) -> u64 { + self.get_config() + .map(|config| config.timeout.unwrap_or(30)) + .unwrap_or(30) + } + + // 获取插件支持的命令 + fn get_command(&self, file_path: Option<&str>) -> String { + if let Some(config) = self.get_config() { + if let Some(run_cmd) = &config.run_command { + if let Some(path) = file_path { + return run_cmd.replace("$filename", path); + } else { + return run_cmd + .split_whitespace() + .next() + .unwrap_or(&config.language) + .to_string(); + } + } + } + self.get_default_command() + } + + // 获取插件配置 + fn get_config(&self) -> Option { + // 获取全局应用配置 + if let Ok(app_config) = get_app_config_internal() { + // 检查是否有插件配置 + if let Some(ref plugins) = app_config.plugins { + // 根据当前插件的语言名称过滤配置 + let language_name = self.get_language_key(); + + // 查找匹配的插件配置 + let found_config = plugins + .iter() + .find(|config| config.language == language_name) + .cloned(); + + debug!( + "执行代码 -> 获取插件 [ {} ] 配置 {:?}", + language_name, found_config + ); + return found_config; + } + } + + // 如果没有找到配置,返回默认配置 + debug!( + "执行代码 -> 插件 [ {} ] 未找到配置,使用默认配置", + self.get_language_key() + ); + Some(self.get_default_config()) + } + + // 检查插件是否启用 + #[allow(dead_code)] + fn is_enabled(&self) -> bool { + self.get_config() + .map(|config| config.enabled) + .unwrap_or(false) + } + fn get_version_args(&self) -> Vec<&'static str>; - fn get_execute_args(&self, file_path: &str) -> Vec; + + fn get_execute_args(&self, file_path: &str) -> Vec { + if let Some(config) = self.get_config() { + if let Some(run_cmd) = &config.run_command { + // 替换 $filename 后分割,跳过第一个元素(命令本身) + let full_cmd = run_cmd.replace("$filename", file_path); + return full_cmd + .split_whitespace() + .skip(1) // 跳过命令部分,只返回参数 + .map(|s| s.to_string()) + .collect(); + } + } + // 默认情况下,文件路径就是唯一的参数 + vec![file_path.to_string()] + } + fn get_path_command(&self) -> String; - // 可选的钩子函数 - fn pre_execute_hook(&self, _code: &str) -> Result { - Ok(_code.to_string()) + // 构建默认配置 + fn get_default_config(&self) -> PluginConfig; + + // 获取默认命令 + fn get_default_command(&self) -> String; + + // 预执行钩子 + fn pre_execute_hook(&self, code: &str) -> Result { + info!( + "执行代码 -> 插件 [ {} ] 处理 pre_execute_hook 开始", + self.get_language_key() + ); + + if let Some(config) = self.get_config() { + // 1. 处理 before_compile 命令(直接在 Rust 中处理) + if let Some(before_cmd) = &config.before_compile { + info!( + "执行代码 -> 插件 [ {} ] 处理 pre_execute_hook 处理环境变量: {}", + self.get_language_key(), + before_cmd + ); + + self.handle_environment_setup(before_cmd)?; + } + + // 2. 切换到 execute_home 目录 + if let Some(execute_home) = self.get_execute_home() { + info!( + "执行代码 -> 插件 [ {} ] 处理 pre_execute_hook 切换到执行目录 {}", + self.get_language_key(), + execute_home.display() + ); + std::env::set_current_dir(&execute_home) + .map_err(|e| format!("切换目录失败: {}", e))?; + } + } + + info!( + "执行代码 -> 插件 [ {} ] 处理 pre_execute_hook 结束", + self.get_language_key() + ); + + Ok(code.to_string()) + } + + fn handle_environment_setup(&self, command: &str) -> Result<(), String> { + // 处理 export 命令(Unix/Linux/macOS) + if command.starts_with("export ") { + return self.handle_export_command(command); + } + + // 处理 set 命令(Windows) + if command.starts_with("set ") { + return self.handle_set_command(command); + } + + // 处理其他通用环境设置 + self.execute_cross_platform_command(command) + } + + fn handle_export_command(&self, command: &str) -> Result<(), String> { + if let Some(env_part) = command.strip_prefix("export ") { + if let Some((key, value)) = env_part.split_once("=") { + let value = value.trim_matches('"').trim_matches('\''); + let expanded_value = self.expand_env_vars(value); + let key = key.trim(); + + // 先记录日志,再设置环境变量 + info!("设置环境变量 {}={}", key, expanded_value); + + // 使用 unsafe 块设置环境变量 + unsafe { + std::env::set_var(key, expanded_value); + } + } + } + Ok(()) + } + + fn handle_set_command(&self, command: &str) -> Result<(), String> { + if let Some(env_part) = command.strip_prefix("set ") { + if let Some((key, value)) = env_part.split_once("=") { + let value = value.trim_matches('"').trim_matches('\''); + let expanded_value = self.expand_env_vars(value); + let key = key.trim(); + + // 先记录日志,再设置环境变量 + info!("设置环境变量 {}={}", key, expanded_value); + + // 使用 unsafe 块设置环境变量 + unsafe { + std::env::set_var(key, expanded_value); + } + } + } + Ok(()) + } + + fn expand_env_vars(&self, value: &str) -> String { + let mut result = value.to_string(); + + // 处理 Unix 风格的环境变量 $VAR + if result.contains("$PATH") { + if let Ok(current_path) = std::env::var("PATH") { + result = result.replace("$PATH", ¤t_path); + } + } + + // 处理 Windows 风格的环境变量 %VAR% + if result.contains("%PATH%") { + if let Ok(current_path) = std::env::var("PATH") { + result = result.replace("%PATH%", ¤t_path); + } + } + + result + } + + fn execute_cross_platform_command(&self, command: &str) -> Result<(), String> { + let output = if cfg!(target_os = "windows") { + std::process::Command::new("cmd") + .args(["/C", command]) + .output() + } else { + std::process::Command::new("sh") + .args(["-c", command]) + .output() + }; + + let output = output.map_err(|e| format!("执行命令失败: {}", e))?; + + if !output.status.success() { + return Err(format!( + "命令执行失败: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + Ok(()) } - fn post_execute_hook(&self, _result: &mut ExecutionResult) -> Result<(), String> { + // 后执行钩子 + fn post_execute_hook(&self, result: &mut ExecutionResult) -> Result<(), String> { + if result.success && result.stdout.is_empty() && result.stderr.is_empty() { + result.stdout = "代码执行成功 (无输出)".to_string(); + } + Ok(()) } } @@ -51,5 +307,4 @@ pub trait LanguagePlugin: Send + Sync { pub mod manager; pub mod python2; pub mod python3; - pub use manager::PluginManager; diff --git a/src-tauri/src/plugins/python2.rs b/src-tauri/src/plugins/python2.rs index 6e387ea..86fa37e 100644 --- a/src-tauri/src/plugins/python2.rs +++ b/src-tauri/src/plugins/python2.rs @@ -1,4 +1,5 @@ -use super::{ExecutionResult, LanguagePlugin}; +use super::{LanguagePlugin, PluginConfig}; +use std::vec; pub struct Python2Plugin; @@ -11,43 +12,33 @@ impl LanguagePlugin for Python2Plugin { "Python 2" } - fn get_file_extension(&self) -> &'static str { - "py" - } - - fn get_commands(&self) -> Vec<&'static str> { - vec!["python", "python2"] + fn get_language_key(&self) -> &'static str { + "python2" } fn get_version_args(&self) -> Vec<&'static str> { vec!["--version"] } - fn get_execute_args(&self, file_path: &str) -> Vec { - vec![file_path.to_string()] - } - fn get_path_command(&self) -> String { "import sys; print(sys.executable)".to_string() } - fn pre_execute_hook(&self, code: &str) -> Result { - Ok(code.to_string()) - } - - fn post_execute_hook(&self, result: &mut ExecutionResult) -> Result<(), String> { - // Python 特定的后处理 - if result.success && result.stdout.is_empty() && result.stderr.is_empty() { - result.stdout = "代码执行成功 (无输出)".to_string(); - } - - // 清理 Python 特定的错误信息 - if !result.stderr.is_empty() { - result.stderr = result - .stderr - .replace("Traceback (most recent call last):", "Error:"); + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: String::from("python2"), + before_compile: None, + extension: String::from("py"), + execute_home: None, + run_command: Option::from(String::from("python2 $filename")), + after_compile: None, + template: None, + timeout: Some(30), } + } - Ok(()) + fn get_default_command(&self) -> String { + self.get_config().unwrap().run_command.unwrap() } } diff --git a/src-tauri/src/plugins/python3.rs b/src-tauri/src/plugins/python3.rs index 55db338..3bb8dfe 100644 --- a/src-tauri/src/plugins/python3.rs +++ b/src-tauri/src/plugins/python3.rs @@ -1,4 +1,4 @@ -use super::{ExecutionResult, LanguagePlugin}; +use super::{LanguagePlugin, PluginConfig}; pub struct Python3Plugin; @@ -11,43 +11,33 @@ impl LanguagePlugin for Python3Plugin { "Python 3" } - fn get_file_extension(&self) -> &'static str { - "py" - } - - fn get_commands(&self) -> Vec<&'static str> { - vec!["python", "python3"] + fn get_language_key(&self) -> &'static str { + "python3" } fn get_version_args(&self) -> Vec<&'static str> { vec!["--version"] } - fn get_execute_args(&self, file_path: &str) -> Vec { - vec![file_path.to_string()] - } - fn get_path_command(&self) -> String { "import sys; print(sys.executable)".to_string() } - fn pre_execute_hook(&self, code: &str) -> Result { - Ok(code.to_string()) - } - - fn post_execute_hook(&self, result: &mut ExecutionResult) -> Result<(), String> { - // Python 特定的后处理 - if result.success && result.stdout.is_empty() && result.stderr.is_empty() { - result.stdout = "代码执行成功 (无输出)".to_string(); - } - - // 清理 Python 特定的错误信息 - if !result.stderr.is_empty() { - result.stderr = result - .stderr - .replace("Traceback (most recent call last):", "Error:"); + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: String::from("python3"), + before_compile: None, + extension: String::from("py"), + execute_home: None, + run_command: Option::from(String::from("python3 $filename")), + after_compile: None, + template: None, + timeout: Some(30), } + } - Ok(()) + fn get_default_command(&self) -> String { + self.get_config().unwrap().run_command.unwrap() } } diff --git a/src-tauri/src/setup/app.rs b/src-tauri/src/setup/app.rs new file mode 100644 index 0000000..6cc1538 --- /dev/null +++ b/src-tauri/src/setup/app.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use tauri::command; + +#[derive(Debug, Serialize, Deserialize)] +pub struct AppInfo { + pub version: String, + pub build_time: String, + pub platform: String, + pub arch: String, +} + +#[command] +pub async fn get_app_info() -> Result { + Ok(AppInfo { + version: env!("CARGO_PKG_VERSION").to_string(), + build_time: env!("BUILD_TIME", "Unknown").to_string(), + platform: std::env::consts::OS.to_string(), + arch: std::env::consts::ARCH.to_string(), + }) +} diff --git a/src-tauri/src/setup/menu.rs b/src-tauri/src/setup/menu.rs new file mode 100644 index 0000000..85ad74a --- /dev/null +++ b/src-tauri/src/setup/menu.rs @@ -0,0 +1,27 @@ +use super::menus; + +use tauri::{ + AppHandle, + menu::{Menu, MenuBuilder}, +}; + +pub fn create_menu(app: &AppHandle) -> tauri::Result> { + // 应用菜单 + let app_submenu = menus::app::create_app_submenu(app)?; + + // 编辑菜单 + let edit_submenu = menus::edit::create_edit_submenu(app)?; + + let menu = MenuBuilder::new(app) + .items(&[&app_submenu, &edit_submenu]) + .build()?; + + Ok(menu) +} + +pub fn setup_menu_handler(app: &AppHandle) { + app.on_menu_event(move |app, event| { + menus::app::handle_app_menu_event(app, event.id().as_ref()); + menus::edit::handle_edit_menu_event(app, event.id().as_ref()); + }); +} diff --git a/src-tauri/src/setup/menus/app.rs b/src-tauri/src/setup/menus/app.rs new file mode 100644 index 0000000..55b3923 --- /dev/null +++ b/src-tauri/src/setup/menus/app.rs @@ -0,0 +1,47 @@ +use log::info; +use tauri::{ + AppHandle, Emitter, + menu::{MenuItemBuilder, Submenu, SubmenuBuilder}, +}; + +pub fn create_app_submenu(app: &AppHandle) -> tauri::Result> { + let about_item = MenuItemBuilder::new("关于 CodeForge") + .id("about") + .build(app)?; + + let quit_item = MenuItemBuilder::new("退出 CodeForge") + .id("quit") + .accelerator("CmdOrCtrl+Q") + .build(app)?; + + let settings_item = MenuItemBuilder::new("设置") + .id("settings") + .accelerator("CmdOrCtrl+,") + .build(app)?; + + let app_submenu = SubmenuBuilder::new(app, "CodeForge") + .item(&about_item) + .separator() + .item(&settings_item) + .separator() + .item(&quit_item) + .build()?; + + Ok(app_submenu) +} + +pub fn handle_app_menu_event(app: &AppHandle, event_id: &str) { + match event_id { + "about" => { + let _event = app.emit("show-about", ()); + } + "settings" => { + let _event = app.emit("show-settings", ()); + } + "quit" => { + info!("CodeForge 应用关闭"); + app.exit(0); + } + _ => {} + } +} diff --git a/src-tauri/src/setup/menus/edit.rs b/src-tauri/src/setup/menus/edit.rs new file mode 100644 index 0000000..0635ee8 --- /dev/null +++ b/src-tauri/src/setup/menus/edit.rs @@ -0,0 +1,70 @@ +use tauri::menu::PredefinedMenuItem; + +use tauri::{ + AppHandle, Manager, + menu::{MenuItemBuilder, Submenu, SubmenuBuilder}, +}; + +pub fn create_edit_submenu(app: &AppHandle) -> tauri::Result> { + let undo_item = MenuItemBuilder::new("撤销") + .id("undo") + .accelerator("CmdOrCtrl+Z") + .build(app)?; + + let redo_item = MenuItemBuilder::new("重做") + .id("redo") + .accelerator("CmdOrCtrl+Shift+Z") + .build(app)?; + + let cut_item = MenuItemBuilder::new("剪切") + .id("cut") + .accelerator("CmdOrCtrl+X") + .build(app)?; + + let copy_item = MenuItemBuilder::new("复制") + .id("copy") + .accelerator("CmdOrCtrl+C") + .build(app)?; + + let select_all_item = MenuItemBuilder::new("全选") + .id("select_all") + .accelerator("CmdOrCtrl+A") + .build(app)?; + + let edit_submenu = SubmenuBuilder::new(app, "编辑") + .item(&undo_item) + .item(&redo_item) + .separator() + .item(&cut_item) + .item(©_item) + .item(&PredefinedMenuItem::paste(app, Option::from("粘贴"))?) + .separator() + .item(&select_all_item) + .build()?; + + Ok(edit_submenu) +} + +pub fn handle_edit_menu_event(app: &AppHandle, event_id: &str) { + let binding = app.webview_windows(); + let webview = binding.values().next().unwrap(); + + match event_id { + "undo" => { + webview.eval("document.execCommand('undo')").ok(); + } + "redo" => { + webview.eval("document.execCommand('redo')").ok(); + } + "cut" => { + webview.eval("document.execCommand('cut')").ok(); + } + "copy" => { + webview.eval("document.execCommand('copy')").ok(); + } + "select_all" => { + webview.eval("document.execCommand('selectAll')").ok(); + } + _ => {} + } +} diff --git a/src-tauri/src/setup/menus/mod.rs b/src-tauri/src/setup/menus/mod.rs new file mode 100644 index 0000000..fe00cba --- /dev/null +++ b/src-tauri/src/setup/menus/mod.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod edit; diff --git a/src-tauri/src/setup/mod.rs b/src-tauri/src/setup/mod.rs new file mode 100644 index 0000000..046a6e2 --- /dev/null +++ b/src-tauri/src/setup/mod.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod menu; +pub mod menus; diff --git a/src-tauri/src/utils/logger.rs b/src-tauri/src/utils/logger.rs new file mode 100644 index 0000000..8888d70 --- /dev/null +++ b/src-tauri/src/utils/logger.rs @@ -0,0 +1,182 @@ +use log::{error, info, warn}; +use std::path::PathBuf; +use std::sync::Mutex; +use tauri::{AppHandle, command}; + +// 全局日志目录状态 +static LOG_DIRECTORY: Mutex> = Mutex::new(None); + +// 获取日志目录 +#[command] +pub async fn get_log_directory(app: AppHandle) -> Result { + let log_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + guard.clone() + }; + + let dir = match log_dir { + Some(dir) => dir, + None => crate::logger::get_log_directory(&app), + }; + + Ok(dir.to_string_lossy().to_string()) +} + +// 设置日志目录 +#[command] +pub async fn set_log_directory(app: AppHandle, path: String) -> Result<(), String> { + let new_path = PathBuf::from(&path); + info!("设置日志 -> 用户设置日志目录为: {}", path); + + // 验证目录是否存在,如果不存在则创建 + if !new_path.exists() { + std::fs::create_dir_all(&new_path).map_err(|e| format!("无法创建目录: {}", e))?; + } + + // 验证是否为目录 + if !new_path.is_dir() { + error!("设置日志 -> 指定的路径不是目录"); + return Err("指定的路径不是目录".to_string()); + } + + // 更新全局日志目录 + { + info!("设置日志 -> 更新全局日志目录"); + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = Some(new_path); + } + + // 保存到配置文件 + if let Ok(mut guard) = crate::config::get_config_manager() { + if let Some(config_manager) = guard.as_mut() { + if let Err(e) = config_manager.set_log_directory(Some(path.clone())) { + warn!("设置日志 -> 保存配置失败: {}", e); + } else { + info!("设置日志 -> 配置已保存到文件"); + } + } + } else { + warn!("设置日志 -> 无法获取配置管理器"); + } + + // 可选:重新初始化日志系统以立即使用新目录 + // 注意:这会导致当前日志文件切换,可能会丢失一些日志 + if let Err(e) = crate::logger::reinit_logger(&app) { + warn!("重新初始化日志系统失败: {}", e); + } + + info!("设置日志 -> 日志目录已设置为: {}", path); + Ok(()) +} + +// 重置日志目录 +#[command] +pub async fn reset_log_directory(_app: AppHandle) -> Result<(), String> { + { + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = None; + } + + // 从配置文件中移除自定义日志目录 + if let Ok(mut guard) = crate::config::get_config_manager() { + if let Some(config_manager) = guard.as_mut() { + if let Err(e) = config_manager.set_log_directory(None) { + warn!("重置日志 -> 保存配置失败: {}", e); + } else { + info!("重置日志 -> 配置已保存到文件"); + } + } + } else { + warn!("重置日志 -> 无法获取配置管理器"); + } + + info!("重置日志 -> 重置日志目录为默认目录"); + Ok(()) +} + +// 获取日志文件列表 +#[command] +pub async fn get_log_files(app: AppHandle) -> Result, String> { + let log_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + match &*guard { + Some(dir) => dir.clone(), + None => crate::logger::get_log_directory(&app), + } + }; + info!("获取日志 -> 日志目录为: {}", log_dir.display()); + + let mut log_files = Vec::new(); + + if let Ok(entries) = std::fs::read_dir(&log_dir) { + for entry in entries.flatten() { + if let Some(filename) = entry.file_name().to_str() { + if filename.ends_with(".log") && filename.starts_with("codeforge-") { + log_files.push(filename.to_string()); + info!("获取日志 -> 发现日志文件: {}", filename); + } + } + } + } + + info!("获取日志 -> 找到 {} 个日志文件", log_files.len()); + log_files.sort(); + log_files.reverse(); // 最新的在前面 + + Ok(log_files) +} + +// 清除日志 +#[command] +pub async fn clear_logs(app: AppHandle, keep_days: u32) -> Result { + use chrono::{Duration, Local}; + + let log_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + match &*guard { + Some(dir) => dir.clone(), + None => crate::logger::get_log_directory(&app), + } + }; + info!("清理日志 -> 日志目录为: {}", log_dir.display()); + + let cutoff_date = Local::now() - Duration::days(keep_days as i64); + let mut deleted_count = 0; + let mut scanned_count = 0; + + if let Ok(entries) = std::fs::read_dir(&log_dir) { + for entry in entries.flatten() { + if let Some(filename) = entry.file_name().to_str() { + if filename.ends_with(".log") && filename.starts_with("codeforge-") { + scanned_count += 1; + // 从文件名提取日期 codeforge-2024-08-09.log + if let Some(date_str) = filename + .strip_prefix("codeforge-") + .and_then(|s| s.strip_suffix(".log")) + { + if let Ok(file_date) = + chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + { + if file_date < cutoff_date.date_naive() { + if let Err(e) = std::fs::remove_file(entry.path()) { + warn!("清理日志 -> 删除日志文件失败 {}: {}", filename, e); + } else { + info!("清理日志 -> 已删除日志文件: {}", filename); + deleted_count += 1; + } + } else { + info!("清理日志 -> 日志文件文件未到期,将被保留: {}", filename); + } + } + } + } + } + } + } + info!( + "清理日志 -> 扫描 {} 个日志文件,删除 {} 个", + scanned_count, deleted_count + ); + + Ok(deleted_count) +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs new file mode 100644 index 0000000..d991728 --- /dev/null +++ b/src-tauri/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod logger; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e818205..0fcf933 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,6 +13,8 @@ "windows": [ { "title": "CodeForge", + "minWidth": 1000, + "minHeight": 600, "width": 1800, "height": 1200, "additionalBrowserArgs": "--disable-context-menu" diff --git a/src/App.vue b/src/App.vue index f2eca06..9cae388 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,7 @@ :supported-languages="supportedLanguages" :current-language="currentLanguage" @run-code="runCode" + @stop-code="stopCode" @clear-output="clearOutput" @language-change="handleLanguageChange" @show-settings="showSettings = true"> @@ -39,26 +40,29 @@ - - - + + + + + + + + \ No newline at end of file + diff --git a/src/components/About.vue b/src/components/About.vue new file mode 100644 index 0000000..d697e89 --- /dev/null +++ b/src/components/About.vue @@ -0,0 +1,161 @@ + + + diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index bbb8a00..91be1e8 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -4,40 +4,42 @@
- +
- + + - + + + +
@@ -45,6 +47,8 @@ \ No newline at end of file + +// 自动滚动到底部(实时输出时) +watch(() => props.output, async (newOutput, oldOutput) => { + // 只有在输出增加时才滚动(避免清空时滚动) + if (newOutput.length > (oldOutput?.length || 0)) { + await nextTick() + if (outputContainer.value) { + outputContainer.value.scrollTop = outputContainer.value.scrollHeight + } + } +}, { flush: 'post' }) + +// 监听运行状态变化,运行开始时也滚动到底部 +watch(() => props.isRunning, async (isRunning) => { + if (isRunning) { + await nextTick() + if (outputContainer.value) { + outputContainer.value.scrollTop = outputContainer.value.scrollHeight + } + } +}) + diff --git a/src/components/Settings.vue b/src/components/Settings.vue new file mode 100644 index 0000000..c6cf322 --- /dev/null +++ b/src/components/Settings.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/components/Toast.vue b/src/components/Toast.vue index 5b331d0..ebb7bf2 100644 --- a/src/components/Toast.vue +++ b/src/components/Toast.vue @@ -1,97 +1,70 @@ diff --git a/src/components/setting/General.vue b/src/components/setting/General.vue new file mode 100644 index 0000000..b064011 --- /dev/null +++ b/src/components/setting/General.vue @@ -0,0 +1,253 @@ + + + diff --git a/src/components/setting/Language.vue b/src/components/setting/Language.vue new file mode 100644 index 0000000..a90ddc1 --- /dev/null +++ b/src/components/setting/Language.vue @@ -0,0 +1,275 @@ +