├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── pull_request_template.md ├── thumbnail.png └── workflows │ ├── build-check.yml │ ├── format-check.yml │ ├── linux-aarch64-nightly.yml │ ├── linux-x86_64-nightly.yml │ ├── macos-nightly.yml │ ├── test-release.yml │ └── windows-nightly.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── app-icon.png ├── docs ├── favicon.ico ├── index.html ├── main.js └── styles.css ├── jsconfig.json ├── package-lock.json ├── package.json ├── screenshot-light.png ├── screenshot.png ├── src-tauri ├── .cargo │ └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── android │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ └── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── ios │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x-1.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ └── AppIcon-83.5x83.5@2x.png ├── src │ ├── commands.rs │ ├── main.rs │ ├── monitoring │ │ ├── mod.rs │ │ ├── process_monitor.rs │ │ ├── system_monitor.rs │ │ └── types.rs │ ├── state.rs │ └── ui │ │ ├── mod.rs │ │ └── window.rs └── tauri.conf.json ├── src ├── App.svelte ├── app.css ├── app.html ├── lib │ ├── components │ │ ├── AppInfo.svelte │ │ ├── ThemeSwitcher.svelte │ │ ├── TitleBar.svelte │ │ ├── index.ts │ │ ├── modals │ │ │ ├── KillProcessModal.svelte │ │ │ ├── Modal.svelte │ │ │ ├── ProcessDetailsModal.svelte │ │ │ └── index.ts │ │ ├── process │ │ │ ├── ActionButtons.svelte │ │ │ ├── ProcessIcon.svelte │ │ │ ├── ProcessRow.svelte │ │ │ ├── ProcessTable.svelte │ │ │ ├── TableHeader.svelte │ │ │ └── index.ts │ │ ├── stats │ │ │ ├── CpuPanel.svelte │ │ │ ├── MemoryPanel.svelte │ │ │ ├── NetworkPanel.svelte │ │ │ ├── PanelHeader.svelte │ │ │ ├── ProgressBar.svelte │ │ │ ├── StatItem.svelte │ │ │ ├── StatPanel.svelte │ │ │ ├── StatsBar.svelte │ │ │ ├── StoragePanel.svelte │ │ │ ├── SystemPanel.svelte │ │ │ └── index.ts │ │ └── toolbar │ │ │ ├── ColumnToggle.svelte │ │ │ ├── PaginationControls.svelte │ │ │ ├── RefreshControls.svelte │ │ │ ├── SearchBox.svelte │ │ │ ├── StatusFilter.svelte │ │ │ ├── ToolBar.svelte │ │ │ └── index.ts │ ├── constants │ │ └── index.ts │ ├── definitions │ │ ├── columns.ts │ │ ├── index.ts │ │ ├── settings.ts │ │ └── themes.ts │ ├── stores │ │ ├── index.ts │ │ ├── processes.ts │ │ ├── settings.ts │ │ └── theme.ts │ ├── types │ │ └── index.ts │ └── utils │ │ └── index.ts └── routes │ ├── +layout.js │ ├── +layout.svelte │ └── +page.svelte ├── static ├── 128x128.png ├── 32x32.png ├── favicon.png ├── svelte.svg ├── tauri.svg └── vite.svg ├── svelte.config.js └── vite.config.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Abdenasser -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NeoHtop 2 | 3 | Thank you for considering contributing to NeoHtop! We welcome contributions from the community. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork the repository. 8 | 2. Create a new branch (`git checkout -b feature/YourFeature`). 9 | 3. Make your changes. 10 | 4. Commit your changes (`git commit -m 'Add some feature'`). 11 | 5. Push to the branch (`git push origin feature/YourFeature`). 12 | 6. Open a pull request. 13 | 14 | ## Code of Conduct 15 | 16 | Please note that this project is released with a [Contributor Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). By participating in this project you agree to abide by its terms. 17 | 18 | ## Reporting Bugs 19 | 20 | Please use the [bug report template](./ISSUE_TEMPLATE/bug_report.md) to report any bugs you find. 21 | 22 | ## Requesting Features 23 | 24 | Please use the [feature request template](./ISSUE_TEMPLATE/feature_request.md) to suggest new features. -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: abdenasser 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support 4 | url: https://github.com/Abdenasser/neohtop/discussions 5 | about: Please use discussions for questions and support. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | ## Checklist: 24 | 25 | - [ ] My code follows the style guidelines of this project 26 | - [ ] I have performed a self-review of my code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] I have added tests that prove my fix is effective or that my feature works 31 | - [ ] New and existing unit tests pass locally with my changes 32 | - [ ] Any dependent changes have been merged and published in downstream modules -------------------------------------------------------------------------------- /.github/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/.github/thumbnail.png -------------------------------------------------------------------------------- /.github/workflows/build-check.yml: -------------------------------------------------------------------------------- 1 | name: Build Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - "src-tauri/**" 8 | - ".github/workflows/**" 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | CARGO_INCREMENTAL: 1 13 | CARGO_NET_RETRY: 10 14 | RUSTUP_MAX_RETRIES: 10 15 | RUST_BACKTRACE: 1 16 | RUSTC_WRAPPER: sccache 17 | CARGO_BUILD_JOBS: 2 18 | 19 | jobs: 20 | build: 21 | name: Build Check 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: "lts/*" 31 | cache: "npm" 32 | 33 | - name: Cache Linux Dependencies 34 | id: cache-apt 35 | uses: actions/cache@v3 36 | with: 37 | path: | 38 | /var/cache/apt/archives/*.deb 39 | /var/lib/apt/lists/* 40 | key: ${{ runner.os }}-apt-${{ hashFiles('**/package.json', '**/Cargo.lock') }} 41 | restore-keys: | 42 | ${{ runner.os }}-apt- 43 | 44 | - name: Add Ubuntu Jammy repo for WebKitGTK 4.0 45 | run: | 46 | echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list 47 | sudo apt update 48 | 49 | - name: Install Linux Dependencies 50 | run: | 51 | sudo rm -rf /var/cache/apt/archives/lock 52 | sudo rm -rf /var/cache/apt/archives/partial 53 | sudo rm -rf /var/lib/apt/lists/lock 54 | sudo rm -rf /var/lib/apt/lists/partial 55 | sudo apt-get update 56 | sudo apt-get install --no-install-recommends -y \ 57 | build-essential \ 58 | pkg-config \ 59 | libgtk-3-dev \ 60 | libayatana-appindicator3-dev \ 61 | librsvg2-dev \ 62 | libglib2.0-dev \ 63 | libjavascriptcoregtk-4.0-dev \ 64 | libsoup-3.0-dev \ 65 | libwebkit2gtk-4.1-dev 66 | 67 | - name: Remove Jammy repo 68 | run: | 69 | sudo sed -i '/jammy main universe/d' /etc/apt/sources.list 70 | sudo apt update 71 | 72 | - name: Install Rust 73 | uses: dtolnay/rust-toolchain@stable 74 | with: 75 | components: cargo 76 | target: x86_64-unknown-linux-gnu 77 | 78 | - name: Install sccache 79 | run: | 80 | SCCACHE_VERSION=v0.7.7 81 | curl -L "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" | tar xz 82 | sudo mv sccache-*/sccache /usr/local/bin/sccache 83 | echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV 84 | 85 | - uses: Swatinem/rust-cache@v2 86 | with: 87 | workspaces: "./src-tauri -> target" 88 | shared-key: "build" 89 | 90 | - name: Install Dependencies 91 | run: npm ci 92 | 93 | - name: Build Application 94 | run: | 95 | npm run tauri build -- \ 96 | --target x86_64-unknown-linux-gnu \ 97 | --bundles deb \ 98 | --ci 99 | -------------------------------------------------------------------------------- /.github/workflows/format-check.yml: -------------------------------------------------------------------------------- 1 | name: Format Check 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | format: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 'lts/*' 17 | 18 | - name: Setup Rust 19 | uses: dtolnay/rust-toolchain@stable 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Check formatting 25 | run: npm run format:check 26 | -------------------------------------------------------------------------------- /.github/workflows/linux-aarch64-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Linux (aarch64) Nightly Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_upload_url: 7 | description: "Release upload URL" 8 | required: true 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | PKG_CONFIG_ALLOW_CROSS: 1 13 | PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig 14 | PKG_CONFIG: /usr/bin/aarch64-linux-gnu-pkg-config 15 | 16 | jobs: 17 | build: 18 | name: Build Linux aarch64 Packages 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: "lts/*" 28 | cache: "npm" 29 | 30 | - name: Install Rust 31 | uses: dtolnay/rust-toolchain@stable 32 | with: 33 | targets: aarch64-unknown-linux-gnu 34 | 35 | - name: Configure ARM64 repositories 36 | run: | 37 | sudo dpkg --add-architecture arm64 38 | # Remove all existing sources 39 | sudo rm -rf /etc/apt/sources.list.d/* 40 | sudo truncate -s 0 /etc/apt/sources.list 41 | # Add only ports.ubuntu.com repository 42 | sudo tee /etc/apt/sources.list << EOF 43 | deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse 44 | deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse 45 | deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse 46 | deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu jammy main restricted universe multiverse 47 | deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse 48 | deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu jammy-security main restricted universe multiverse 49 | EOF 50 | sudo apt-get update 51 | # Install required packages including cross-compilation tools 52 | sudo apt-get install -y \ 53 | build-essential \ 54 | pkg-config \ 55 | crossbuild-essential-arm64 \ 56 | gcc-aarch64-linux-gnu \ 57 | g++-aarch64-linux-gnu \ 58 | libgtk-3-dev:arm64 \ 59 | libayatana-appindicator3-dev:arm64 \ 60 | librsvg2-dev:arm64 \ 61 | libglib2.0-dev:arm64 \ 62 | libjavascriptcoregtk-4.0-dev:arm64 \ 63 | libsoup-3.0-dev:arm64 \ 64 | libwebkit2gtk-4.1-dev:arm64 \ 65 | libssl-dev:arm64 \ 66 | libssl-dev \ 67 | openssl:arm64 68 | # Configure pkg-config for cross-compilation 69 | echo "PKG_CONFIG=/usr/bin/aarch64-linux-gnu-pkg-config" >> $GITHUB_ENV 70 | echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV 71 | 72 | - name: Install Dependencies 73 | run: npm install 74 | 75 | - name: Setup cross-compilation environment 76 | run: | 77 | sudo apt-get install -y \ 78 | crossbuild-essential-arm64 \ 79 | pkg-config \ 80 | libssl-dev:arm64 \ 81 | libssl-dev \ 82 | openssl:arm64 \ 83 | file \ 84 | desktop-file-utils \ 85 | libfuse2 \ 86 | qemu-user-static 87 | 88 | # Setup pkg-config 89 | sudo tee /usr/bin/aarch64-linux-gnu-pkg-config << 'EOF' 90 | #!/bin/sh 91 | export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig 92 | exec pkg-config "$@" 93 | EOF 94 | sudo chmod +x /usr/bin/aarch64-linux-gnu-pkg-config 95 | 96 | # Create .cargo/config 97 | mkdir -p .cargo 98 | cat > .cargo/config << EOF 99 | [target.aarch64-unknown-linux-gnu] 100 | linker = "aarch64-linux-gnu-gcc" 101 | ar = "aarch64-linux-gnu-ar" 102 | EOF 103 | 104 | # Download and setup appimagetool for ARM64 105 | wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage 106 | chmod +x appimagetool-aarch64.AppImage 107 | sudo mv appimagetool-aarch64.AppImage /usr/local/bin/appimagetool 108 | 109 | # Set environment variables 110 | echo "PKG_CONFIG=/usr/bin/aarch64-linux-gnu-pkg-config" >> $GITHUB_ENV 111 | echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV 112 | echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV 113 | echo "OPENSSL_INCLUDE_DIR=/usr/include/aarch64-linux-gnu" >> $GITHUB_ENV 114 | echo "OPENSSL_LIB_DIR=/usr/lib/aarch64-linux-gnu" >> $GITHUB_ENV 115 | echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV 116 | echo "APPIMAGE_EXTRACT_AND_RUN=1" >> $GITHUB_ENV 117 | 118 | - name: Build Frontend 119 | run: npm run build 120 | 121 | - name: Build AppImage 122 | run: | 123 | echo "Building AppImage for aarch64..." 124 | npm run tauri build -- --target aarch64-unknown-linux-gnu --bundles appimage 125 | cd src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/appimage/ 126 | for f in *.AppImage; do 127 | echo "AARCH64_APPIMAGE_PATH=src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/appimage/$f" >> $GITHUB_ENV 128 | done 129 | 130 | - name: Build Debian Package 131 | run: | 132 | echo "Building Debian package for aarch64..." 133 | npm run tauri build -- --target aarch64-unknown-linux-gnu --bundles deb 134 | cd src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/ 135 | for f in *.deb; do 136 | echo "AARCH64_DEB_PATH=src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/$f" >> $GITHUB_ENV 137 | done 138 | 139 | - name: Build RPM Package 140 | run: | 141 | echo "Building RPM package for aarch64..." 142 | npm run tauri build -- --target aarch64-unknown-linux-gnu --bundles rpm 143 | cd src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/rpm/ 144 | for f in *.rpm; do 145 | echo "AARCH64_RPM_PATH=src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/rpm/$f" >> $GITHUB_ENV 146 | done 147 | 148 | - name: Get version from package.json 149 | id: version 150 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 151 | 152 | - name: Upload AppImage to Release 153 | if: github.event.inputs.release_upload_url != '' 154 | uses: actions/upload-release-asset@v1 155 | env: 156 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 157 | with: 158 | upload_url: ${{ github.event.inputs.release_upload_url }} 159 | asset_path: ${{ env.AARCH64_APPIMAGE_PATH }} 160 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_aarch64.AppImage 161 | asset_content_type: application/x-executable 162 | 163 | - name: Upload Debian Package to Release 164 | if: github.event.inputs.release_upload_url != '' 165 | uses: actions/upload-release-asset@v1 166 | env: 167 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 168 | with: 169 | upload_url: ${{ github.event.inputs.release_upload_url }} 170 | asset_path: ${{ env.AARCH64_DEB_PATH }} 171 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_aarch64.deb 172 | asset_content_type: application/vnd.debian.binary-package 173 | 174 | - name: Upload RPM Package to Release 175 | if: github.event.inputs.release_upload_url != '' 176 | uses: actions/upload-release-asset@v1 177 | env: 178 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 179 | with: 180 | upload_url: ${{ github.event.inputs.release_upload_url }} 181 | asset_path: ${{ env.AARCH64_RPM_PATH }} 182 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_aarch64.rpm 183 | asset_content_type: application/x-rpm 184 | -------------------------------------------------------------------------------- /.github/workflows/linux-x86_64-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Linux (x86_64) Nightly Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_upload_url: 7 | description: "Release upload URL" 8 | required: true 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | name: Build Linux Packages 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: "lts/*" 25 | cache: "npm" 26 | 27 | - name: Install Rust 28 | uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Add Ubuntu Jammy repo for WebKitGTK 4.0 31 | run: | 32 | echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list 33 | sudo apt update 34 | 35 | - name: Install Linux Dependencies 36 | run: | 37 | sudo apt-get update 38 | sudo apt-get install -y \ 39 | build-essential \ 40 | pkg-config \ 41 | libgtk-3-dev \ 42 | libayatana-appindicator3-dev \ 43 | librsvg2-dev \ 44 | libglib2.0-dev \ 45 | libjavascriptcoregtk-4.0-dev \ 46 | libsoup-3.0-dev \ 47 | libwebkit2gtk-4.1-dev 48 | 49 | - name: Remove Jammy repo 50 | run: | 51 | sudo sed -i '/jammy main universe/d' /etc/apt/sources.list 52 | sudo apt update 53 | 54 | - name: Install Dependencies 55 | run: | 56 | npm install 57 | 58 | - name: Build Frontend 59 | run: npm run build 60 | 61 | - name: Build AppImage (x86_64) 62 | run: | 63 | echo "Building AppImage for x86_64..." 64 | npm run tauri build -- --target x86_64-unknown-linux-gnu --bundles appimage 65 | cd src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/ 66 | for f in *.AppImage; do 67 | echo "APPIMAGE_PATH=src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/$f" >> $GITHUB_ENV 68 | done 69 | 70 | - name: Build Debian Package (x86_64) 71 | run: | 72 | echo "Building Debian package for x86_64..." 73 | npm run tauri build -- --target x86_64-unknown-linux-gnu --bundles deb 74 | cd src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/ 75 | for f in *.deb; do 76 | echo "DEB_PATH=src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/$f" >> $GITHUB_ENV 77 | done 78 | 79 | - name: Build RPM Package (x86_64) 80 | run: | 81 | echo "Building RPM package for x86_64..." 82 | npm run tauri build -- --target x86_64-unknown-linux-gnu --bundles rpm 83 | cd src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/ 84 | for f in *.rpm; do 85 | echo "RPM_PATH=src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/$f" >> $GITHUB_ENV 86 | done 87 | 88 | - name: Get version from package.json 89 | id: version 90 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 91 | 92 | - name: Upload AppImage to Release 93 | if: github.event.inputs.release_upload_url != '' 94 | uses: actions/upload-release-asset@v1 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 97 | with: 98 | upload_url: ${{ github.event.inputs.release_upload_url }} 99 | asset_path: ${{ env.APPIMAGE_PATH }} 100 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_x86_64.AppImage 101 | asset_content_type: application/x-executable 102 | 103 | - name: Upload Debian Package to Release 104 | if: github.event.inputs.release_upload_url != '' 105 | uses: actions/upload-release-asset@v1 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 108 | with: 109 | upload_url: ${{ github.event.inputs.release_upload_url }} 110 | asset_path: ${{ env.DEB_PATH }} 111 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_x86_64.deb 112 | asset_content_type: application/vnd.debian.binary-package 113 | 114 | - name: Upload RPM Package to Release 115 | if: github.event.inputs.release_upload_url != '' 116 | uses: actions/upload-release-asset@v1 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 119 | with: 120 | upload_url: ${{ github.event.inputs.release_upload_url }} 121 | asset_path: ${{ env.RPM_PATH }} 122 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_x86_64.rpm 123 | asset_content_type: application/x-rpm 124 | -------------------------------------------------------------------------------- /.github/workflows/macos-nightly.yml: -------------------------------------------------------------------------------- 1 | name: MacOS (Intel/Apple Silicon) Nightly Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_upload_url: 7 | description: 'Release upload URL' 8 | required: true 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | name: Build MacOS Apps 16 | runs-on: macos-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 'lts/*' 25 | cache: 'npm' 26 | 27 | - name: Install Rust 28 | uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Install Dependencies 31 | run: | 32 | rustup target add x86_64-apple-darwin 33 | rustup target add aarch64-apple-darwin 34 | npm install 35 | 36 | - name: Set up keychain 37 | run: | 38 | security create-keychain -p "" build.keychain 39 | security default-keychain -s build.keychain 40 | security unlock-keychain -p "" build.keychain 41 | echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/certificate.p12 42 | security import /tmp/certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign 43 | security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain 44 | env: 45 | MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} 46 | MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} 47 | 48 | - name: Build Frontend 49 | run: npm run build 50 | 51 | - name: Build for Intel Mac 52 | run: | 53 | echo "Building for Intel Mac..." 54 | npm run tauri build -- --target x86_64-apple-darwin --bundles dmg --config "{\"bundle\":{\"macOS\":{\"signingIdentity\": \"Developer ID Application: Abdenasser Elidrissi (785JV74B9Y)\"}}}" 55 | # Rename the Intel build and store the filename 56 | cd src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/ 57 | for f in *.dmg; do 58 | mv "$f" "intel-$f" 59 | echo "INTEL_DMG_PATH=src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/intel-$f" >> $GITHUB_ENV 60 | done 61 | env: 62 | APPLE_ID: ${{ secrets.APPLE_ID }} 63 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 64 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 65 | 66 | - name: Build for Apple Silicon 67 | run: | 68 | echo "Building for aarch64..." 69 | npm run tauri build -- --target aarch64-apple-darwin --bundles dmg --config "{\"bundle\":{\"macOS\":{\"signingIdentity\": \"Developer ID Application: Abdenasser Elidrissi (785JV74B9Y)\"}}}" 70 | # Rename the Apple Silicon build and store the filename 71 | cd src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/ 72 | for f in *.dmg; do 73 | mv "$f" "silicon-$f" 74 | echo "SILICON_DMG_PATH=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/silicon-$f" >> $GITHUB_ENV 75 | done 76 | env: 77 | APPLE_ID: ${{ secrets.APPLE_ID }} 78 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 79 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 80 | 81 | - name: Get version from package.json 82 | id: version 83 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 84 | 85 | - name: Upload Intel Build to Release 86 | if: github.event.inputs.release_upload_url != '' 87 | uses: actions/upload-release-asset@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 90 | with: 91 | upload_url: ${{ github.event.inputs.release_upload_url }} 92 | asset_path: ${{ env.INTEL_DMG_PATH }} 93 | asset_name: intel-NeoHtop_${{ steps.version.outputs.version }}_x64.dmg 94 | asset_content_type: application/x-apple-diskimage 95 | 96 | - name: Upload Silicon Build to Release 97 | if: github.event.inputs.release_upload_url != '' 98 | uses: actions/upload-release-asset@v1 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 101 | with: 102 | upload_url: ${{ github.event.inputs.release_upload_url }} 103 | asset_path: ${{ env.SILICON_DMG_PATH }} 104 | asset_name: silicon-NeoHtop_${{ steps.version.outputs.version }}_aarch64.dmg 105 | asset_content_type: application/x-apple-diskimage -------------------------------------------------------------------------------- /.github/workflows/test-release.yml: -------------------------------------------------------------------------------- 1 | name: Test Release Build 2 | 3 | on: 4 | workflow_dispatch: # Manual trigger 5 | 6 | jobs: 7 | create-draft: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Get version from package.json 13 | id: version 14 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 15 | 16 | - name: Create Draft Release 17 | id: create_release 18 | uses: softprops/action-gh-release@v1 19 | with: 20 | name: "NeoHtop v${{ steps.version.outputs.version }}" 21 | tag_name: "v${{ steps.version.outputs.version }}" 22 | draft: true 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 25 | 26 | - name: Trigger MacOS Build 27 | uses: benc-uk/workflow-dispatch@v1 28 | with: 29 | workflow: macos-nightly.yml 30 | token: ${{ secrets.PAT_TOKEN }} 31 | inputs: '{"release_upload_url": "${{ steps.create_release.outputs.upload_url }}"}' 32 | 33 | - name: Trigger Windows Build 34 | uses: benc-uk/workflow-dispatch@v1 35 | with: 36 | workflow: windows-nightly.yml 37 | token: ${{ secrets.PAT_TOKEN }} 38 | inputs: '{"release_upload_url": "${{ steps.create_release.outputs.upload_url }}"}' 39 | 40 | - name: Trigger Linux x86_64 Build 41 | uses: benc-uk/workflow-dispatch@v1 42 | with: 43 | workflow: linux-x86_64-nightly.yml 44 | token: ${{ secrets.PAT_TOKEN }} 45 | inputs: '{"release_upload_url": "${{ steps.create_release.outputs.upload_url }}"}' 46 | -------------------------------------------------------------------------------- /.github/workflows/windows-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Windows (x86_64) Nightly Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_upload_url: 7 | description: 'Release upload URL' 8 | required: true 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | name: Build Windows Executable 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 'lts/*' 25 | cache: 'npm' 26 | 27 | - name: Install Rust 28 | uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Install WebView2 31 | run: | 32 | $WebView2InstallPath = "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" 33 | Invoke-WebRequest "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile $WebView2InstallPath 34 | Start-Process -FilePath $WebView2InstallPath -Args "/silent /install" -Wait 35 | 36 | - name: Install Dependencies 37 | run: | 38 | npm install 39 | 40 | - name: Build Frontend 41 | run: npm run build 42 | 43 | - name: Build Windows Executable 44 | shell: bash # Force using bash shell for consistent environment variable setting 45 | run: | 46 | echo "Building Windows executable..." 47 | npm run tauri build 48 | echo "WIN_EXE_PATH=src-tauri/target/release/NeoHtop.exe" >> $GITHUB_ENV 49 | 50 | - name: Get version from package.json 51 | id: version 52 | shell: bash # Force using bash shell 53 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 54 | 55 | - name: Upload to Release 56 | if: github.event.inputs.release_upload_url != '' 57 | uses: actions/upload-release-asset@v1 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 60 | with: 61 | upload_url: ${{ github.event.inputs.release_upload_url }} 62 | asset_path: ${{ env.WIN_EXE_PATH }} 63 | asset_name: NeoHtop_${{ steps.version.outputs.version }}_x64.exe 64 | asset_content_type: application/vnd.microsoft.portable-executable -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | 12 | ## Jetbrains 13 | .idea/ 14 | .run/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm exec lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-svelte"], 3 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "svelte.svelte-vscode", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "svelte.enable-ts-plugin": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Abdenasser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | NeoHtop Logo 4 |

NeoHtop

5 |

A modern, cross-platform system monitor built on top of Svelte, Rust, and Tauri.

6 | 7 | [![License](https://img.shields.io/github/license/Abdenasser/neohtop)](https://github.com/Abdenasser/neohtop/blob/main/LICENSE) 8 | [![GitHub stars](https://img.shields.io/github/stars/Abdenasser/neohtop)](https://github.com/Abdenasser/neohtop/stargazers) 9 | [![GitHub issues](https://img.shields.io/github/issues/Abdenasser/neohtop)](https://github.com/Abdenasser/neohtop/issues) 10 | [![GitHub release](https://img.shields.io/github/v/release/Abdenasser/neohtop)](https://github.com/Abdenasser/neohtop/releases) 11 | [![Notarized by Apple](https://img.shields.io/badge/Release_Notarized_by_Apple-000000?style=flat-square&logo=apple&logoColor=white)](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) 12 |
13 | 14 |
15 | 16 | 18 | NeoHtop Screenshot 19 | 20 |
21 | 22 |
23 |

If you find this project helpful, consider buying me a coffee:

24 | Buy Me A Coffee 25 |

Or sponsor me on GitHub:

26 | Sponsor @abdenasser 27 | 28 |
29 | 30 | ## Table of Contents 31 | - [Why NeoHtop?](#why-neohtop) 32 | - [Features](#features) 33 | - [Tech Stack](#tech-stack) 34 | - [Getting Started](#getting-started) 35 | - [Prerequisites](#prerequisites) 36 | - [Installation](#installation) 37 | - [Running with Sudo](#running-with-sudo) 38 | - [Development](#development) 39 | - [Setup](#setup) 40 | - [Code Formatting](#code-formatting) 41 | - [Pull Requests](#pull-requests) 42 | - [Contributing](#contributing) 43 | - [License](#license) 44 | 45 | ## Why NeoHtop? 46 | [Read about the back story and motivation behind NeoHtop](https://www.abdenasser.com/2024/11/06/oh-boy-neohtop/) 47 | 48 | ## Features 49 | - 🚀 Real-time process monitoring 50 | - 💻 CPU and Memory usage tracking 51 | - 🎨 Beautiful, modern UI with dark/light themes 52 | - 🔍 Advanced process search and filtering 53 | - 📌 Pin important processes 54 | - 🛠 Process management (kill processes) 55 | - 🎯 Sort by any column 56 | - 🔄 Auto-refresh system stats 57 | 58 | ### Search Functionality 59 | Search for processes by name, command, or PID. Use commas to search for multiple terms simultaneously. Regular expressions are supported for advanced filtering. 60 | 61 | Examples: 62 | - `arm, x86`: Returns processes with "arm" or "x86" in the name or command 63 | - `d$`: Lists daemons (processes ending with 'd') 64 | - `^(\w+\.)+\w+$`: Shows processes with reverse domain name notation (e.g., com.docker.vmnetd) 65 | 66 | ## Tech Stack 67 | - **Frontend**: SvelteKit, TypeScript 68 | - **Backend**: Rust, Tauri 69 | - **Styling**: CSS Variables for theming 70 | - **Icons**: FontAwesome 71 | 72 | ## Getting Started 73 | 74 | ### Prerequisites 75 | - Node.js (v16 or later) 76 | - Rust (latest stable) 77 | - Xcode Command Line Tools (for macOS) 78 | 79 | ### Installation 80 | Download the latest release from the [releases page](https://github.com/Abdenasser/neohtop/releases). 81 | 82 | ### Running with Sudo 83 | Some processes require monitoring with sudo privileges. To monitor these processes, launch NeoHtop with sudo: 84 | 85 | - macOS: `sudo /Applications/NeoHtop.app/Contents/MacOS/NeoHtop` 86 | - Linux: `pkexec /path/to/neohtop` (recommended) 87 | 88 | ## Development 89 | 90 | ### Setup 91 | ```bash 92 | # Install dependencies 93 | npm install 94 | 95 | # Run in development mode 96 | npm run tauri dev 97 | 98 | # Build for production 99 | npm run tauri build 100 | ``` 101 | 102 | ### Code Formatting 103 | We use Prettier for web code and `cargo fmt` for Rust code. 104 | 105 | ```bash 106 | # Format all files 107 | npm run format 108 | 109 | # Check formatting without making changes 110 | npm run format:check 111 | ``` 112 | 113 | ### Pull Requests 114 | Before submitting a PR, ensure: 115 | 1. All code is formatted (`npm run format`) 116 | 2. The format check passes (`npm run format:check`) 117 | 3. Your commits follow the project's commit message conventions 118 | 119 | ## Contributing 120 | We welcome contributions! Please see our [contributing guidelines](./.github/CONTRIBUTING.md) for more information. 121 | 122 | ## License 123 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 124 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/app-icon.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/docs/favicon.ico -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | // =============================== 2 | // Theme Management 3 | // =============================== 4 | const themeToggle = document.getElementById('themeToggle'); 5 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); 6 | 7 | function setTheme(isDark) { 8 | document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); 9 | themeToggle.querySelector('.theme-icon').textContent = isDark ? '☀️' : '🌙'; 10 | localStorage.setItem('theme', isDark ? 'dark' : 'light'); 11 | } 12 | 13 | // Initialize theme 14 | const savedTheme = localStorage.getItem('theme'); 15 | if (savedTheme) { 16 | setTheme(savedTheme === 'dark'); 17 | } else { 18 | setTheme(prefersDark.matches); 19 | } 20 | 21 | // Theme event listeners 22 | themeToggle.addEventListener('click', () => { 23 | const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 24 | setTheme(!isDark); 25 | }); 26 | 27 | prefersDark.addEventListener('change', (e) => { 28 | if (!localStorage.getItem('theme')) { 29 | setTheme(e.matches); 30 | } 31 | }); 32 | 33 | // =============================== 34 | // Version and Download Management 35 | // =============================== 36 | async function fetchDownloadStats() { 37 | try { 38 | const releasesResponse = await fetch('https://api.github.com/repos/abdenasser/neohtop/releases'); 39 | const releases = await releasesResponse.json(); 40 | const githubDownloads = releases.reduce((total, release) => { 41 | const releaseDownloads = release.assets.reduce((sum, asset) => 42 | sum + asset.download_count, 0); 43 | return total + releaseDownloads; 44 | }, 0); 45 | 46 | const brewResponse = await fetch('https://formulae.brew.sh/api/analytics/install/homebrew-core/365d.json'); 47 | const brewData = await brewResponse.json(); 48 | const brewInstalls = brewData.formulae?.neohtop?.[0]?.count || 0; 49 | 50 | const totalDownloads = githubDownloads + brewInstalls; 51 | document.getElementById('download-count').textContent = new Intl.NumberFormat().format(totalDownloads); 52 | } catch (error) { 53 | console.error('Failed to fetch download stats:', error); 54 | document.getElementById('download-count').textContent = 'N/A'; 55 | } 56 | } 57 | 58 | async function updateVersion() { 59 | try { 60 | const response = await fetch('https://api.github.com/repos/Abdenasser/neohtop/releases/latest'); 61 | const data = await response.json(); 62 | const version = data.tag_name; 63 | const versionNumber = version.match(/\d+\.\d+\.\d+/)?.[0]; 64 | 65 | if (versionNumber) { 66 | document.getElementById('current-version').textContent = "v" + versionNumber; 67 | updateDownloadLinks(versionNumber); 68 | } 69 | } catch (error) { 70 | console.error('Failed to fetch version:', error); 71 | } 72 | } 73 | 74 | function updateDownloadLinks(versionNumber) { 75 | const platformUrls = { 76 | 'macos-intel': `intel-NeoHtop_${versionNumber}_x64.dmg`, 77 | 'macos-silicon': `silicon-NeoHtop_${versionNumber}_aarch64.dmg`, 78 | 'windows': `NeoHtop_${versionNumber}_x64.exe`, 79 | 'linux-deb-x64': `NeoHtop_${versionNumber}_x86_64.deb`, 80 | 'linux-appimage-x64': `NeoHtop_${versionNumber}_x86_64.AppImage`, 81 | 'linux-rpm-x64': `NeoHtop_${versionNumber}_x86_64.rpm`, 82 | 'linux-deb-arm64': `NeoHtop_${versionNumber}_aarch64.deb`, 83 | 'linux-appimage-arm64': `NeoHtop_${versionNumber}_aarch64.AppImage`, 84 | 'linux-rpm-arm64': `NeoHtop_${versionNumber}_aarch64.rpm` 85 | }; 86 | 87 | document.querySelectorAll('.download-button').forEach(link => { 88 | const platform = link.getAttribute('data-type'); 89 | if (platformUrls[platform]) { 90 | link.href = `https://github.com/Abdenasser/neohtop/releases/download/v${versionNumber}/${platformUrls[platform]}`; 91 | } 92 | }); 93 | } 94 | 95 | // =============================== 96 | // UI Interactions 97 | // =============================== 98 | // FAQ Accordion 99 | document.querySelectorAll('.faq-question').forEach(button => { 100 | button.addEventListener('click', () => { 101 | const faqItem = button.parentElement; 102 | const isActive = faqItem.classList.contains('active'); 103 | document.querySelectorAll('.faq-item').forEach(item => item.classList.remove('active')); 104 | if (!isActive) faqItem.classList.add('active'); 105 | }); 106 | }); 107 | 108 | // Download tracking 109 | document.querySelectorAll('.download-button').forEach(button => { 110 | button.addEventListener('click', (e) => { 111 | gtag('event', 'download', { 112 | 'event_category': 'App', 113 | 'event_label': button.getAttribute('data-type'), 114 | 'value': button.getAttribute('data-version') 115 | }); 116 | }); 117 | }); 118 | 119 | // Smooth scroll 120 | document.querySelectorAll('a[href^="#"]').forEach(anchor => { 121 | anchor.addEventListener('click', function (e) { 122 | e.preventDefault(); 123 | const target = document.querySelector(this.getAttribute('href')); 124 | if (target) { 125 | target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 126 | } 127 | }); 128 | }); 129 | // =============================== 130 | // Animations 131 | // =============================== 132 | const observer = new IntersectionObserver( 133 | (entries) => { 134 | entries.forEach(entry => { 135 | if (entry.isIntersecting) { 136 | entry.target.classList.add('visible'); 137 | } 138 | }); 139 | }, 140 | { threshold: 0.1 } 141 | ); 142 | 143 | document.querySelectorAll('.feature, .step, .download-button').forEach(el => { 144 | observer.observe(el); 145 | }); 146 | 147 | // =============================== 148 | // Initialization 149 | // =============================== 150 | document.addEventListener('DOMContentLoaded', () => { 151 | updateVersion(); 152 | fetchDownloadStats(); 153 | }); 154 | 155 | 156 | // =============================== 157 | // Mobile Navigation 158 | // =============================== 159 | 160 | document.addEventListener('DOMContentLoaded', () => { 161 | const menuButton = document.querySelector('.menu-button'); 162 | const navLinks = document.querySelector('.nav-links'); 163 | 164 | menuButton.addEventListener('click', () => { 165 | navLinks.classList.toggle('active'); 166 | }); 167 | 168 | // Close menu when clicking outside 169 | document.addEventListener('click', (e) => { 170 | if (!navLinks.contains(e.target) && !menuButton.contains(e.target)) { 171 | navLinks.classList.remove('active'); 172 | } 173 | }); 174 | }); -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neohtop", 3 | "version": "1.1.3", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", 12 | "tauri": "tauri", 13 | "format": "prettier --write ./src && cargo fmt --manifest-path src-tauri/Cargo.toml", 14 | "format:check": "prettier --check ./src && cargo fmt --manifest-path src-tauri/Cargo.toml -- --check", 15 | "prepare": "husky install" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 20 | "@fortawesome/free-solid-svg-icons": "^6.6.0", 21 | "@tauri-apps/api": "^2.0.3", 22 | "@tauri-apps/plugin-os": "^2.0.0", 23 | "@tauri-apps/plugin-shell": "^2.2.1", 24 | "simple-icons": "^13.15.0", 25 | "svelte-fa": "^4.0.3" 26 | }, 27 | "devDependencies": { 28 | "@sveltejs/adapter-static": "^3.0.5", 29 | "@sveltejs/kit": "^2.7.0", 30 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 31 | "@tauri-apps/cli": "^2.0.4", 32 | "husky": "^8.0.0", 33 | "lint-staged": "^15.2.10", 34 | "prettier": "^3.3.3", 35 | "prettier-plugin-svelte": "^3.2.7", 36 | "svelte": "^5.0.0", 37 | "svelte-check": "^4.0.0", 38 | "typescript": "^5.5.0", 39 | "vite": "^6.3.5" 40 | }, 41 | "lint-staged": { 42 | "src/**/*.{js,ts,jsx,tsx,svelte}": [ 43 | "prettier --write" 44 | ], 45 | "src-tauri/**/*.rs": [ 46 | "cargo fmt --manifest-path src-tauri/Cargo.toml --" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /screenshot-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/screenshot-light.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/screenshot.png -------------------------------------------------------------------------------- /src-tauri/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-apple-darwin] 2 | rustflags = [ 3 | "-C", "link-arg=-undefined", 4 | "-C", "link-arg=dynamic_lookup", 5 | ] 6 | 7 | [target.aarch64-apple-darwin] 8 | rustflags = [ 9 | "-C", "link-arg=-undefined", 10 | "-C", "link-arg=dynamic_lookup", 11 | ] 12 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "neohtop" 3 | version = "1.1.3" 4 | description = "A cross-platform system monitor" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | [build-dependencies] 9 | tauri-build = { version = "2", features = [] } 10 | 11 | [dependencies] 12 | serde_json = "1.0" 13 | serde = { version = "1.0", features = ["derive"] } 14 | tauri = { version = "2", features = ["macos-private-api"] } 15 | sysinfo = "0.29.0" 16 | tauri-plugin-shell = "2" 17 | tauri-plugin-os = "2" 18 | window-vibrancy = "0.5.2" 19 | 20 | [features] 21 | default = [ "custom-protocol" ] 22 | custom-protocol = [ "tauri/custom-protocol" ] 23 | 24 | [profile.release] 25 | panic = "abort" # Strip expensive panic clean-up logic 26 | codegen-units = 1 # Compile crates one after another so the compiler can optimize better 27 | lto = "fat" # More aggressive link-time optimization 28 | opt-level = 3 # Optimize for maximum performance 29 | strip = true # Remove debug symbols 30 | incremental = false # Disable incremental compilation 31 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "core:app:allow-version", 11 | "core:window:allow-start-dragging", 12 | "core:window:allow-maximize", 13 | "core:window:allow-minimize", 14 | "core:window:allow-close", 15 | "shell:default" 16 | ] 17 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/src/commands.rs: -------------------------------------------------------------------------------- 1 | //! Tauri command handlers 2 | //! 3 | //! This module contains the command handlers that are exposed to the frontend 4 | //! through Tauri's IPC mechanism. These commands provide the interface between 5 | //! the frontend and the system monitoring functionality. 6 | 7 | use crate::monitoring::{ProcessInfo, ProcessMonitor, SystemStats}; 8 | use crate::state::AppState; 9 | use sysinfo::SystemExt; 10 | use tauri::State; 11 | 12 | /// Retrieves the current list of processes and system statistics 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `state` - The application state containing system monitoring components 17 | /// 18 | /// # Returns 19 | /// 20 | /// A tuple containing: 21 | /// * A vector of process information 22 | /// * Current system statistics 23 | /// 24 | /// # Errors 25 | /// 26 | /// Returns an error string if: 27 | /// * Failed to acquire locks on system state 28 | /// * Failed to collect process information 29 | #[tauri::command] 30 | pub async fn get_processes( 31 | state: State<'_, AppState>, 32 | ) -> Result<(Vec, SystemStats), String> { 33 | let mut sys = state.sys.lock().map_err(|e| e.to_string())?; 34 | sys.refresh_all(); 35 | sys.refresh_networks_list(); 36 | sys.refresh_disks_list(); 37 | 38 | let mut process_monitor = state.process_monitor.lock().map_err(|e| e.to_string())?; 39 | let mut system_monitor = state.system_monitor.lock().map_err(|e| e.to_string())?; 40 | 41 | let processes = process_monitor.collect_processes(&sys)?; 42 | let system_stats = system_monitor.collect_stats(&sys); 43 | 44 | Ok((processes, system_stats)) 45 | } 46 | 47 | /// Attempts to kill a process with the specified PID 48 | /// 49 | /// # Arguments 50 | /// 51 | /// * `pid` - Process ID to kill 52 | /// * `state` - The application state 53 | /// 54 | /// # Returns 55 | /// 56 | /// * `true` if the process was successfully killed 57 | /// * `false` if the process couldn't be killed or wasn't found 58 | /// 59 | /// # Errors 60 | /// 61 | /// Returns an error string if failed to acquire lock on system state 62 | #[tauri::command] 63 | pub async fn kill_process(pid: u32, state: State<'_, AppState>) -> Result { 64 | let sys = state.sys.lock().map_err(|e| e.to_string())?; 65 | Ok(ProcessMonitor::kill_process(&sys, pid)) 66 | } 67 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | //! NeoHtop - A modern system monitor built with Tauri 3 | //! 4 | //! This is the main entry point for the application. It sets up the Tauri 5 | //! application, initializes plugins, and configures window effects. 6 | 7 | mod commands; 8 | mod monitoring; 9 | mod state; 10 | mod ui; 11 | 12 | use state::AppState; 13 | use tauri::Manager; 14 | 15 | /// Main entry point for the application 16 | /// 17 | /// # Panics 18 | /// 19 | /// Will panic if: 20 | /// - Unable to create the main window 21 | /// - Failed to apply window effects 22 | /// - Failed to initialize the application state 23 | fn main() { 24 | tauri::Builder::default() 25 | .setup(|app| { 26 | let window = app.get_webview_window("main").unwrap(); 27 | ui::setup_window_effects(&window).expect("Failed to apply window effects"); 28 | Ok(()) 29 | }) 30 | .plugin(tauri_plugin_shell::init()) 31 | .plugin(tauri_plugin_os::init()) 32 | .manage(AppState::new()) 33 | .invoke_handler(tauri::generate_handler![ 34 | commands::get_processes, 35 | commands::kill_process, 36 | ]) 37 | .run(tauri::generate_context!()) 38 | .expect("error while running tauri application"); 39 | } 40 | -------------------------------------------------------------------------------- /src-tauri/src/monitoring/mod.rs: -------------------------------------------------------------------------------- 1 | //! System monitoring functionality 2 | //! 3 | //! This module provides types and functionality for monitoring system resources 4 | //! and processes. It includes process monitoring, system statistics collection, 5 | //! and data structures for representing system state. 6 | 7 | mod process_monitor; 8 | mod system_monitor; 9 | mod types; 10 | 11 | pub use process_monitor::ProcessMonitor; 12 | pub use system_monitor::SystemMonitor; 13 | pub use types::*; // Re-export all types 14 | -------------------------------------------------------------------------------- /src-tauri/src/monitoring/process_monitor.rs: -------------------------------------------------------------------------------- 1 | //! Process monitoring functionality 2 | //! 3 | //! This module handles monitoring and managing system processes, including 4 | //! collecting process information and managing process lifecycle. 5 | 6 | use super::{ProcessData, ProcessInfo, ProcessStaticInfo}; 7 | use std::collections::HashMap; 8 | use std::fmt::Debug; 9 | use std::time::{SystemTime, UNIX_EPOCH}; 10 | use sysinfo::{PidExt, ProcessExt, ProcessStatus, SystemExt}; 11 | 12 | /// Monitors and manages system processes 13 | #[derive(Debug)] 14 | pub struct ProcessMonitor { 15 | /// Cache for static process information to avoid redundant allocations 16 | process_cache: HashMap, 17 | } 18 | 19 | impl ProcessMonitor { 20 | /// Creates a new process monitor instance 21 | pub fn new() -> Self { 22 | Self { 23 | process_cache: HashMap::new(), 24 | } 25 | } 26 | 27 | /// Collects information about all running processes 28 | /// 29 | /// # Arguments 30 | /// 31 | /// * `sys` - System information provider 32 | /// 33 | /// # Returns 34 | /// 35 | /// A vector of process information, or an error string if collection failed 36 | pub fn collect_processes(&mut self, sys: &sysinfo::System) -> Result, String> { 37 | let current_time = Self::get_current_time()?; 38 | let processes_data = self.collect_process_data(sys, current_time); 39 | Ok(self.build_process_info(processes_data)) 40 | } 41 | 42 | /// Attempts to kill a process 43 | /// 44 | /// # Arguments 45 | /// 46 | /// * `sys` - System information provider 47 | /// * `pid` - Process ID to kill 48 | /// 49 | /// # Returns 50 | /// 51 | /// Boolean indicating whether the process was successfully killed 52 | pub fn kill_process(sys: &sysinfo::System, pid: u32) -> bool { 53 | sys.process(sysinfo::Pid::from(pid as usize)) 54 | .map(|process| process.kill()) 55 | .unwrap_or(false) 56 | } 57 | 58 | /// Gets the current system time in seconds since UNIX epoch 59 | fn get_current_time() -> Result { 60 | SystemTime::now() 61 | .duration_since(UNIX_EPOCH) 62 | .map(|d| d.as_secs()) 63 | .map_err(|e| format!("Failed to get system time: {}", e)) 64 | } 65 | 66 | /// Collects raw process data from the system 67 | fn collect_process_data(&self, sys: &sysinfo::System, current_time: u64) -> Vec { 68 | sys.processes() 69 | .iter() 70 | .map(|(pid, process)| { 71 | let start_time = process.start_time(); 72 | ProcessData { 73 | pid: pid.as_u32(), 74 | name: process.name().to_string(), 75 | cmd: process.cmd().to_vec(), 76 | user_id: process.user_id().map(|uid| uid.to_string()), 77 | cpu_usage: process.cpu_usage(), 78 | memory: process.memory(), 79 | status: process.status(), 80 | ppid: process.parent().map(|p| p.as_u32()), 81 | environ: process.environ().to_vec(), 82 | root: process.root().to_string_lossy().into_owned(), 83 | virtual_memory: process.virtual_memory(), 84 | start_time, 85 | run_time: if start_time > 0 { 86 | current_time.saturating_sub(start_time) 87 | } else { 88 | 0 89 | }, 90 | disk_usage: process.disk_usage(), 91 | session_id: process.session_id().map(|id| id.as_u32()), 92 | } 93 | }) 94 | .collect() 95 | } 96 | 97 | /// Builds process information from raw process data 98 | fn build_process_info(&mut self, processes: Vec) -> Vec { 99 | processes 100 | .into_iter() 101 | .map(|data| { 102 | let cached_info = 103 | self.process_cache 104 | .entry(data.pid) 105 | .or_insert_with(|| ProcessStaticInfo { 106 | name: data.name.clone(), 107 | command: data.cmd.join(" "), 108 | user: data.user_id.unwrap_or_else(|| "-".to_string()), 109 | }); 110 | 111 | ProcessInfo { 112 | pid: data.pid, 113 | ppid: data.ppid.unwrap_or(0), 114 | name: cached_info.name.clone(), 115 | cpu_usage: data.cpu_usage, 116 | memory_usage: data.memory, 117 | status: Self::format_status(data.status), 118 | user: cached_info.user.clone(), 119 | command: cached_info.command.clone(), 120 | threads: None, 121 | environ: data.environ, 122 | root: data.root, 123 | virtual_memory: data.virtual_memory, 124 | start_time: data.start_time, 125 | run_time: data.run_time, 126 | disk_usage: (data.disk_usage.read_bytes, data.disk_usage.written_bytes), 127 | session_id: data.session_id, 128 | } 129 | }) 130 | .collect() 131 | } 132 | 133 | /// Formats process status into a human-readable string 134 | pub fn format_status(status: ProcessStatus) -> String { 135 | match status { 136 | ProcessStatus::Run => "Running", 137 | ProcessStatus::Sleep => "Sleeping", 138 | ProcessStatus::Idle => "Idle", 139 | _ => "Unknown", 140 | } 141 | .to_string() 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use super::*; 148 | use sysinfo::System; 149 | 150 | /// Tests creation of a new process monitor 151 | #[test] 152 | fn test_process_monitor_creation() { 153 | let monitor = ProcessMonitor::new(); 154 | assert!(monitor.process_cache.is_empty()); 155 | } 156 | 157 | /// Tests process collection functionality 158 | #[test] 159 | fn test_process_collection() { 160 | let mut monitor = ProcessMonitor::new(); 161 | let mut sys = System::new(); 162 | sys.refresh_all(); 163 | 164 | let result = monitor.collect_processes(&sys); 165 | assert!(result.is_ok()); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src-tauri/src/monitoring/system_monitor.rs: -------------------------------------------------------------------------------- 1 | //! System statistics monitoring 2 | //! 3 | //! This module handles collection and monitoring of system-wide statistics 4 | //! including CPU, memory, network, and disk usage. 5 | 6 | use super::SystemStats; 7 | use std::fmt::Debug; 8 | use std::path::Path; 9 | use std::time::Instant; 10 | use sysinfo::{CpuExt, Disk, DiskExt, NetworkExt, NetworksExt, SystemExt}; 11 | 12 | /// Monitors system-wide statistics 13 | #[derive(Debug)] 14 | pub struct SystemMonitor { 15 | /// Tracks network usage between updates 16 | last_network_update: (Instant, u64, u64), 17 | } 18 | 19 | impl SystemMonitor { 20 | /// Creates a new system monitor instance 21 | /// 22 | /// # Arguments 23 | /// 24 | /// * `sys` - System information provider for initial readings 25 | pub fn new(sys: &sysinfo::System) -> Self { 26 | let initial_rx: u64 = sys 27 | .networks() 28 | .iter() 29 | .map(|(_, data)| data.total_received()) 30 | .sum(); 31 | let initial_tx: u64 = sys 32 | .networks() 33 | .iter() 34 | .map(|(_, data)| data.total_transmitted()) 35 | .sum(); 36 | 37 | Self { 38 | last_network_update: (Instant::now(), initial_rx, initial_tx), 39 | } 40 | } 41 | 42 | /// Collects current system statistics 43 | /// 44 | /// # Arguments 45 | /// 46 | /// * `sys` - System information provider 47 | pub fn collect_stats(&mut self, sys: &sysinfo::System) -> SystemStats { 48 | let (network_rx, network_tx) = self.calculate_network_stats(sys); 49 | let (disk_total, disk_used, disk_free) = self.calculate_disk_stats(sys); 50 | 51 | SystemStats { 52 | cpu_usage: sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(), 53 | memory_total: sys.total_memory(), 54 | memory_used: sys.used_memory(), 55 | memory_free: sys.total_memory() - sys.used_memory(), 56 | memory_cached: sys.total_memory() 57 | - (sys.used_memory() + (sys.total_memory() - sys.used_memory())), 58 | uptime: sys.uptime(), 59 | load_avg: [ 60 | sys.load_average().one, 61 | sys.load_average().five, 62 | sys.load_average().fifteen, 63 | ], 64 | network_rx_bytes: network_rx, 65 | network_tx_bytes: network_tx, 66 | disk_total_bytes: disk_total, 67 | disk_used_bytes: disk_used, 68 | disk_free_bytes: disk_free, 69 | } 70 | } 71 | 72 | /// Filters disks based on platform-specific criteria 73 | #[cfg(not(target_os = "windows"))] 74 | fn filter_disks(disks: &[Disk]) -> Vec<&Disk> { 75 | disks 76 | .iter() 77 | .filter(|disk| disk.mount_point() == Path::new("/")) 78 | .collect() 79 | } 80 | 81 | /// Windows-specific disk filtering 82 | #[cfg(target_os = "windows")] 83 | fn filter_disks(disks: &[Disk]) -> Vec<&Disk> { 84 | disks.iter().collect() 85 | } 86 | 87 | /// Calculates network usage rates 88 | fn calculate_network_stats(&mut self, sys: &sysinfo::System) -> (u64, u64) { 89 | let current_rx: u64 = sys 90 | .networks() 91 | .iter() 92 | .map(|(_, data)| data.total_received()) 93 | .sum(); 94 | let current_tx: u64 = sys 95 | .networks() 96 | .iter() 97 | .map(|(_, data)| data.total_transmitted()) 98 | .sum(); 99 | 100 | let elapsed = self.last_network_update.0.elapsed().as_secs_f64(); 101 | let rx_rate = ((current_rx - self.last_network_update.1) as f64 / elapsed) as u64; 102 | let tx_rate = ((current_tx - self.last_network_update.2) as f64 / elapsed) as u64; 103 | 104 | self.last_network_update = (Instant::now(), current_rx, current_tx); 105 | (rx_rate, tx_rate) 106 | } 107 | 108 | /// Calculates disk usage statistics 109 | fn calculate_disk_stats(&self, sys: &sysinfo::System) -> (u64, u64, u64) { 110 | let disks = Self::filter_disks(sys.disks()); 111 | let total: u64 = disks.iter().map(|disk| disk.total_space()).sum(); 112 | let used: u64 = disks 113 | .iter() 114 | .map(|disk| disk.total_space() - disk.available_space()) 115 | .sum(); 116 | let free: u64 = disks.iter().map(|disk| disk.available_space()).sum(); 117 | (total, used, free) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | use sysinfo::System; 125 | 126 | /// Tests creation of system monitor 127 | #[test] 128 | fn test_system_monitor_creation() { 129 | let sys = System::new(); 130 | let monitor = SystemMonitor::new(&sys); 131 | assert!(monitor.last_network_update.1 >= 0); 132 | assert!(monitor.last_network_update.2 >= 0); 133 | } 134 | 135 | /// Tests system statistics collection 136 | #[test] 137 | fn test_stats_collection() { 138 | let mut sys = System::new(); 139 | let mut monitor = SystemMonitor::new(&sys); 140 | sys.refresh_all(); 141 | 142 | let stats = monitor.collect_stats(&sys); 143 | assert!(!stats.cpu_usage.is_empty()); 144 | assert!(stats.memory_total > 0); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src-tauri/src/monitoring/types.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::fmt::Debug; 3 | use sysinfo::{DiskUsage, ProcessStatus}; 4 | 5 | /// Internal representation of process data collected from the system 6 | /// This struct is used internally and not exposed directly to the frontend 7 | #[derive(Clone, Debug)] 8 | pub(crate) struct ProcessData { 9 | /// Process ID 10 | pub pid: u32, 11 | /// Name of the process 12 | pub name: String, 13 | /// Complete command line arguments 14 | pub cmd: Vec, 15 | /// User ID that owns the process 16 | pub user_id: Option, 17 | /// CPU usage as percentage (0-100) 18 | pub cpu_usage: f32, 19 | /// Physical memory usage in bytes 20 | pub memory: u64, 21 | /// Current process status 22 | pub status: ProcessStatus, 23 | /// Parent process ID 24 | pub ppid: Option, 25 | /// Environment variables 26 | pub environ: Vec, 27 | /// Root directory of the process 28 | pub root: String, 29 | /// Virtual memory usage in bytes 30 | pub virtual_memory: u64, 31 | /// Process start time (Unix timestamp) 32 | pub start_time: u64, 33 | /// Process running time in seconds 34 | pub run_time: u64, 35 | /// Disk I/O statistics 36 | pub disk_usage: DiskUsage, 37 | /// Session ID of the process 38 | pub session_id: Option, 39 | } 40 | 41 | /// Static information about a process that doesn't change frequently 42 | /// Used for caching purposes to avoid frequent updates of stable data 43 | #[derive(Clone, Debug)] 44 | pub struct ProcessStaticInfo { 45 | /// Process name 46 | pub name: String, 47 | /// Full command string 48 | pub command: String, 49 | /// Username of the process owner 50 | pub user: String, 51 | } 52 | 53 | /// Process information exposed to the frontend via Tauri 54 | /// Contains formatted and filtered process data for UI consumption 55 | #[derive(Serialize, Debug)] 56 | pub struct ProcessInfo { 57 | /// Process ID 58 | pub pid: u32, 59 | /// Parent process ID 60 | pub ppid: u32, 61 | /// Process name 62 | pub name: String, 63 | /// CPU usage as percentage (0-100) 64 | pub cpu_usage: f32, 65 | /// Physical memory usage in bytes 66 | pub memory_usage: u64, 67 | /// Process status as string 68 | pub status: String, 69 | /// Username of the process owner 70 | pub user: String, 71 | /// Full command string 72 | pub command: String, 73 | /// Number of threads (if available) 74 | pub threads: Option, 75 | /// Environment variables 76 | pub environ: Vec, 77 | /// Root directory of the process 78 | pub root: String, 79 | /// Virtual memory usage in bytes 80 | pub virtual_memory: u64, 81 | /// Process start time (Unix timestamp) 82 | pub start_time: u64, 83 | /// Process running time in seconds 84 | pub run_time: u64, 85 | /// Disk I/O statistics (read bytes, written bytes) 86 | pub disk_usage: (u64, u64), 87 | /// Session ID of the process 88 | pub session_id: Option, 89 | } 90 | 91 | /// System-wide statistics exposed to the frontend 92 | /// Provides overall system resource usage and performance metrics 93 | #[derive(Serialize, Debug)] 94 | pub struct SystemStats { 95 | /// CPU usage per core as percentage (0-100) 96 | pub cpu_usage: Vec, 97 | /// Total physical memory in bytes 98 | pub memory_total: u64, 99 | /// Used physical memory in bytes 100 | pub memory_used: u64, 101 | /// Free physical memory in bytes 102 | pub memory_free: u64, 103 | /// Cached memory in bytes 104 | pub memory_cached: u64, 105 | /// System uptime in seconds 106 | pub uptime: u64, 107 | /// Load averages for 1, 5, and 15 minutes 108 | pub load_avg: [f64; 3], 109 | /// Total bytes received over network 110 | pub network_rx_bytes: u64, 111 | /// Total bytes transmitted over network 112 | pub network_tx_bytes: u64, 113 | /// Total disk space in bytes 114 | pub disk_total_bytes: u64, 115 | /// Used disk space in bytes 116 | pub disk_used_bytes: u64, 117 | /// Free disk space in bytes 118 | pub disk_free_bytes: u64, 119 | } 120 | -------------------------------------------------------------------------------- /src-tauri/src/state.rs: -------------------------------------------------------------------------------- 1 | //! Application state management 2 | //! 3 | //! This module handles the global application state, including system monitoring 4 | //! and process tracking capabilities. 5 | 6 | use crate::monitoring::{ProcessMonitor, SystemMonitor}; 7 | use std::sync::Mutex; 8 | use sysinfo::{System, SystemExt}; 9 | 10 | /// Global application state 11 | /// 12 | /// Maintains thread-safe access to system information and monitoring components 13 | #[derive(Debug)] 14 | pub struct AppState { 15 | /// System information handler 16 | pub sys: Mutex, 17 | /// Process monitoring component 18 | pub process_monitor: Mutex, 19 | /// System statistics monitoring component 20 | pub system_monitor: Mutex, 21 | } 22 | 23 | impl AppState { 24 | /// Creates a new instance of the application state 25 | /// 26 | /// Initializes system monitoring and process tracking components 27 | /// 28 | /// # Returns 29 | /// 30 | /// A new `AppState` instance with initialized monitors 31 | pub fn new() -> Self { 32 | let mut sys = System::new(); 33 | sys.refresh_all(); 34 | 35 | Self { 36 | process_monitor: Mutex::new(ProcessMonitor::new()), 37 | system_monitor: Mutex::new(SystemMonitor::new(&sys)), 38 | sys: Mutex::new(sys), 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src-tauri/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | //! User interface functionality 2 | //! 3 | //! This module handles UI-specific functionality, including window effects 4 | //! and platform-specific visual customizations. 5 | 6 | mod window; 7 | pub use window::setup_window_effects; 8 | -------------------------------------------------------------------------------- /src-tauri/src/ui/window.rs: -------------------------------------------------------------------------------- 1 | //! Window effects and customization 2 | //! 3 | //! Provides platform-specific window effects like transparency and vibrancy. 4 | 5 | use tauri::WebviewWindow; 6 | 7 | #[cfg(target_os = "windows")] 8 | use window_vibrancy::apply_acrylic; 9 | #[cfg(target_os = "macos")] 10 | use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState}; 11 | 12 | /// Applies Windows-specific window effects 13 | #[cfg(target_os = "windows")] 14 | pub fn setup_window_effects(window: &WebviewWindow) -> Result<(), Box> { 15 | apply_acrylic(window, Some((0, 0, 25, 125)))?; 16 | Ok(()) 17 | } 18 | 19 | /// Applies macOS-specific window effects 20 | #[cfg(target_os = "macos")] 21 | pub fn setup_window_effects(window: &WebviewWindow) -> Result<(), Box> { 22 | apply_vibrancy( 23 | window, 24 | NSVisualEffectMaterial::HudWindow, 25 | Some(NSVisualEffectState::Active), 26 | None, 27 | )?; 28 | Ok(()) 29 | } 30 | 31 | /// No-op for platforms without specific window effects 32 | #[cfg(not(any(target_os = "windows", target_os = "macos")))] 33 | pub fn setup_window_effects(_window: &WebviewWindow) -> Result<(), Box> { 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build", 5 | "beforeDevCommand": "npm run dev", 6 | "frontendDist": "../build", 7 | "devUrl": "http://localhost:1420" 8 | }, 9 | "bundle": { 10 | "active": true, 11 | "icon": [ 12 | "icons/32x32.png", 13 | "icons/128x128.png", 14 | "icons/128x128@2x.png", 15 | "icons/icon.icns", 16 | "icons/icon.ico" 17 | ], 18 | "macOS": { 19 | "hardenedRuntime": true, 20 | "entitlements": null, 21 | "providerShortName": null 22 | }, 23 | "targets": ["app", "dmg"] 24 | }, 25 | "productName": "NeoHtop", 26 | "mainBinaryName": "NeoHtop", 27 | "version": "1.1.3", 28 | "identifier": "com.neohtop.dev", 29 | "plugins": { 30 | "os": { 31 | "all": true 32 | } 33 | }, 34 | "app": { 35 | "macOSPrivateApi": true, 36 | "windows": [ 37 | { 38 | "fullscreen": false, 39 | "theme": "Dark", 40 | "height": 900, 41 | "resizable": true, 42 | "title": "NeoHtop", 43 | "width": 1280, 44 | "minWidth": 1120, 45 | "minHeight": 700, 46 | "titleBarStyle": "Overlay", 47 | "hiddenTitle": true, 48 | "transparent": true 49 | } 50 | ], 51 | "security": { 52 | "csp": null 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/src/App.svelte -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Default theme values will be overridden by theme store */ 3 | --base: #1e1e2e; 4 | --mantle: #181825; 5 | --crust: #11111b; 6 | --text: #cdd6f4; 7 | --subtext0: #a6adc8; 8 | --subtext1: #bac2de; 9 | --surface0: #313244; 10 | --surface1: #45475a; 11 | --surface2: #585b70; 12 | --overlay0: #6c7086; 13 | --overlay1: #7f849c; 14 | --blue: #89b4fa; 15 | --lavender: #b4befe; 16 | --sapphire: #74c7ec; 17 | --sky: #89dceb; 18 | --red: #f38ba8; 19 | --maroon: #eba0ac; 20 | --peach: #fab387; 21 | --yellow: #f9e2af; 22 | --green: #a6e3a1; 23 | --teal: #94e2d5; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | padding: 0; 29 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, 30 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 31 | background-color: var(--base); 32 | color: var(--text); 33 | -webkit-font-smoothing: antialiased; 34 | overflow: hidden; 35 | user-select: none; 36 | } 37 | 38 | /* Global scrollbar styles */ 39 | * { 40 | scrollbar-width: thin; 41 | scrollbar-color: var(--surface2) var(--mantle); 42 | } 43 | 44 | ::-webkit-scrollbar { 45 | width: 8px; 46 | height: 8px; 47 | } 48 | 49 | ::-webkit-scrollbar-track { 50 | background: var(--mantle); 51 | border-radius: 4px; 52 | } 53 | 54 | ::-webkit-scrollbar-thumb { 55 | background: var(--surface2); 56 | border-radius: 4px; 57 | transition: background 0.2s ease; 58 | } 59 | 60 | ::-webkit-scrollbar-thumb:hover { 61 | background: var(--surface1); 62 | } 63 | 64 | ::-webkit-scrollbar-corner { 65 | background: var(--mantle); 66 | } 67 | 68 | /* Glassy theme */ 69 | 70 | [data-theme="glassy"] body { 71 | background: transparent !important; 72 | } 73 | 74 | [data-theme="glassy"] .toolbar { 75 | position: relative; 76 | background: rgba(24, 24, 37, 0.5) !important; 77 | border: 1px solid rgba(255, 255, 255, 0.1) !important; 78 | z-index: 9; 79 | } 80 | 81 | [data-theme="glassy"] .stat-panel { 82 | background: rgba(24, 24, 37, 0.2) !important; 83 | z-index: 100; 84 | } 85 | 86 | [data-theme="glassy"] .panel-header { 87 | border-color: rgba(255, 255, 255, 0.1) !important; 88 | } 89 | 90 | [data-theme="glassy"] .col-actions { 91 | background: rgba(24, 24, 37, 0.2) !important; 92 | border-bottom: 1px solid rgba(232, 232, 232, 0.1) !important; 93 | border-left: 1px solid rgba(232, 232, 232, 0.1) !important; 94 | } 95 | 96 | [data-theme="glassy"] .search-input, 97 | [data-theme="glassy"] .btn-toggle, 98 | [data-theme="glassy"] .btn-action, 99 | [data-theme="glassy"] .info-button, 100 | [data-theme="glassy"] .btn-page, 101 | [data-theme="glassy"] .theme-button, 102 | [data-theme="glassy"] .usage-pill, 103 | [data-theme="glassy"] .bar-container, 104 | [data-theme="glassy"] .select-input { 105 | background: rgba(24, 24, 37, 0.2) !important; 106 | z-index: 100; 107 | } 108 | 109 | [data-theme="glassy"] .btn-clear { 110 | z-index: 100; 111 | } 112 | 113 | [data-theme="glassy"] tr.pinned, 114 | [data-theme="glassy"] tr:hover { 115 | background: rgba(24, 24, 37, 0.3) !important; 116 | } 117 | 118 | [data-theme="glassy"] th { 119 | background: rgba(24, 24, 37, 0.5) !important; 120 | } 121 | 122 | [data-theme="glassy"] td { 123 | border-bottom: 1px solid rgba(232, 232, 232, 0.1) !important; 124 | background: rgba(24, 24, 37, 0.2) !important; 125 | } -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NeoHtop 8 | %sveltekit.head% 9 | 10 | 11 | 12 |
%sveltekit.body%
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/components/AppInfo.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |
61 | 62 | 72 | 73 | {#if showInfo} 74 | 75 |
(showInfo = false)}> 76 |
77 |
{ASCII_ART}
78 |
79 |
80 | NeoHtop v{version} 81 | {#if hasUpdate} 82 | 88 | Update to v{latestVersion} 89 | 90 | {/if} 91 |
92 |
93 | app 94 | :: 95 | {APP_INFO.name} 96 |
97 |
98 | source 99 | :: 100 | 106 | {APP_INFO.github} 107 | 108 |
109 |
110 | stack 111 | :: 112 | {APP_INFO.stack.join(", ")} 113 |
114 |
115 |
116 |
117 | {/if} 118 |
119 | 120 | 251 | -------------------------------------------------------------------------------- /src/lib/components/ThemeSwitcher.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | 58 | 59 | {#if showMenu} 60 | 61 | 62 |
(showMenu = false)} 66 | > 67 | {#each themeGroups as group} 68 |
69 |
{group.label}
70 | {#each group.themes as themeName} 71 | {@const theme = themes[themeName]} 72 | 96 | {/each} 97 |
98 | {/each} 99 |
100 | {/if} 101 |
102 | 103 | 227 | -------------------------------------------------------------------------------- /src/lib/components/TitleBar.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 |
6 | NeoHtop 7 |
NeoHtop
8 |
9 |
10 | 11 | 47 | -------------------------------------------------------------------------------- /src/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./toolbar"; 2 | export * from "./process"; 3 | export * from "./stats"; 4 | export * from "./modals"; 5 | export { default as AppInfo } from "./AppInfo.svelte"; 6 | export { default as TitleBar } from "./TitleBar.svelte"; 7 | export { default as ThemeSwitcher } from "./ThemeSwitcher.svelte"; 8 | -------------------------------------------------------------------------------- /src/lib/components/modals/KillProcessModal.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {#if process} 18 |
19 |

Are you sure you want to end this process?

20 |
21 | {process.name} 22 | (PID: {process.pid}) 23 |
24 |
25 | 28 | 36 |
37 |
38 | {/if} 39 |
40 | 41 | 120 | -------------------------------------------------------------------------------- /src/lib/components/modals/Modal.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if show} 9 | 10 | 11 | 22 | {/if} 23 | 24 | 79 | -------------------------------------------------------------------------------- /src/lib/components/modals/ProcessDetailsModal.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 31 | {#if process} 32 | 217 | {/if} 218 | 219 | 220 | 515 | -------------------------------------------------------------------------------- /src/lib/components/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Modal } from "./Modal.svelte"; 2 | export { default as ProcessDetailsModal } from "./ProcessDetailsModal.svelte"; 3 | export { default as KillProcessModal } from "./KillProcessModal.svelte"; 4 | -------------------------------------------------------------------------------- /src/lib/components/process/ActionButtons.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 |
19 | 27 | 34 | 41 |
42 | 43 | 44 | 165 | -------------------------------------------------------------------------------- /src/lib/components/process/ProcessIcon.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 77 | 78 | 85 | -------------------------------------------------------------------------------- /src/lib/components/process/ProcessRow.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | {#each columns.filter((col) => col.visible) as column} 17 | 18 | {#if column.id === "name"} 19 |
20 | 21 | {process.name} 22 |
23 | {:else if column.format} 24 | {@html column.format(process[column.id])} 25 | {:else} 26 | {process[column.id]} 27 | {/if} 28 | 29 | {/each} 30 | 37 | 38 | 39 | 80 | -------------------------------------------------------------------------------- /src/lib/components/process/ProcessTable.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 | 20 | 21 | {#each processes as process (process.pid)} 22 | 50 || 27 | process.memory_usage / (systemStats?.memory_total || 0) > 0.1} 28 | {onTogglePin} 29 | {onShowDetails} 30 | {onKillProcess} 31 | /> 32 | {/each} 33 | 34 |
35 |
36 | 37 | 78 | -------------------------------------------------------------------------------- /src/lib/components/process/TableHeader.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | {#each columns.filter((col) => col.visible) as column} 17 | onToggleSort(column.id)}> 18 |
19 | {column.label} 20 | 24 | {getSortIndicator(column.id)} 25 | 26 |
27 | 28 | {/each} 29 | Actions 30 | 31 | 32 | 33 | 80 | -------------------------------------------------------------------------------- /src/lib/components/process/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ProcessTable } from "./ProcessTable.svelte"; 2 | export { default as ProcessRow } from "./ProcessRow.svelte"; 3 | export { default as TableHeader } from "./TableHeader.svelte"; 4 | export { default as ActionButtons } from "./ActionButtons.svelte"; 5 | export { default as ProcessIcon } from "./ProcessIcon.svelte"; 6 | -------------------------------------------------------------------------------- /src/lib/components/stats/CpuPanel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 |
16 | {#each cpuUsage as usage, i} 17 |
18 | 24 |
25 | {/each} 26 |
27 |
28 | 29 | 60 | -------------------------------------------------------------------------------- /src/lib/components/stats/MemoryPanel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 19 |
20 |
21 | 27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 57 | -------------------------------------------------------------------------------- /src/lib/components/stats/NetworkPanel.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 35 | -------------------------------------------------------------------------------- /src/lib/components/stats/PanelHeader.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |

{title}

13 | {#if usageValue} 14 |
{usageValue}
15 | {/if} 16 |
17 | 18 | 51 | -------------------------------------------------------------------------------- /src/lib/components/stats/ProgressBar.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
19 | {label} 20 |
21 |
25 |
26 | {Math.round(value)}% 27 |
28 | 29 | 81 | -------------------------------------------------------------------------------- /src/lib/components/stats/StatItem.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | {label} 8 | {value} 9 |
10 | 11 | 31 | -------------------------------------------------------------------------------- /src/lib/components/stats/StatPanel.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

{title}

8 |
9 | 10 |
11 |
12 | 13 | 35 | -------------------------------------------------------------------------------- /src/lib/components/stats/StatsBar.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {#if systemStats} 16 |
17 | 18 | 19 | 24 | 25 | 30 | 31 | 32 | 33 | 37 |
38 | {/if} 39 |
40 | 41 | 53 | -------------------------------------------------------------------------------- /src/lib/components/stats/StoragePanel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 | 43 | -------------------------------------------------------------------------------- /src/lib/components/stats/SystemPanel.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 38 | -------------------------------------------------------------------------------- /src/lib/components/stats/index.ts: -------------------------------------------------------------------------------- 1 | export { default as StatsBar } from "./StatsBar.svelte"; 2 | export { default as CpuPanel } from "./CpuPanel.svelte"; 3 | export { default as MemoryPanel } from "./MemoryPanel.svelte"; 4 | export { default as StoragePanel } from "./StoragePanel.svelte"; 5 | export { default as SystemPanel } from "./SystemPanel.svelte"; 6 | export { default as NetworkPanel } from "./NetworkPanel.svelte"; 7 | export { default as PanelHeader } from "./PanelHeader.svelte"; 8 | export { default as ProgressBar } from "./ProgressBar.svelte"; 9 | export { default as StatItem } from "./StatItem.svelte"; 10 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/ColumnToggle.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 | 45 | 46 | {#if showColumnMenu} 47 | 48 |
(showColumnMenu = false)}> 49 | {#each columns as column} 50 | 60 | {/each} 61 |
62 | {/if} 63 |
64 | 65 | 173 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/PaginationControls.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 38 | 39 | 73 |
74 | 75 | 149 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/RefreshControls.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 31 | 43 |
44 | 45 | 110 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/SearchBox.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 91 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/StatusFilter.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 28 |
29 | 30 | 61 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/ToolBar.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 | 34 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 | 50 | 69 | -------------------------------------------------------------------------------- /src/lib/components/toolbar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ToolBar } from "./ToolBar.svelte"; 2 | export { default as SearchBox } from "./SearchBox.svelte"; 3 | export { default as StatusFilter } from "./StatusFilter.svelte"; 4 | export { default as PaginationControls } from "./PaginationControls.svelte"; 5 | export { default as ColumnToggle } from "./ColumnToggle.svelte"; 6 | export { default as RefreshControls } from "./RefreshControls.svelte"; 7 | -------------------------------------------------------------------------------- /src/lib/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const ASCII_ART = ` 2 | ███╗ ██╗███████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ 3 | ████╗ ██║██╔════╝██╔═══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗ 4 | ██╔██╗ ██║█████╗ ██║ ██║███████║ ██║ ██║ ██║██████╔╝ 5 | ██║╚██╗██║██╔══╝ ██║ ██║██╔══██║ ██║ ██║ ██║██╔═══╝ 6 | ██║ ╚████║███████╗╚██████╔╝██║ ██║ ██║ ╚██████╔╝██║ 7 | ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ 8 | `; 9 | 10 | export const APP_INFO = { 11 | name: "NeoHtop", 12 | developer: "Abdenasser", 13 | github: "https://github.com/Abdenasser/neohtop", 14 | stack: ["Tauri", "Rust", "Svelte", "TypeScript"], 15 | }; 16 | 17 | export const ITEMS_PER_PAGE_OPTIONS = [15, 25, 50, 100, 250, 500]; 18 | 19 | export const REFRESH_RATE_OPTIONS = [ 20 | { value: 1000, label: "1s" }, 21 | { value: 2000, label: "2s" }, 22 | { value: 5000, label: "5s" }, 23 | { value: 10000, label: "10s" }, 24 | { value: 30000, label: "30s" }, 25 | ]; 26 | 27 | export const STATUS_OPTIONS = [ 28 | { value: "all", label: "All Statuses" }, 29 | { value: "running", label: "Running" }, 30 | { value: "sleeping", label: "Sleeping" }, 31 | { value: "idle", label: "Idle" }, 32 | { value: "unknown", label: "Unknown" }, 33 | ]; 34 | 35 | export const THEME_GROUPS = [ 36 | { 37 | label: "Dark", 38 | themes: [ 39 | "catppuccin", 40 | "dracula", 41 | "monokaiPro", 42 | "tokyoNight", 43 | "solarizedDark", 44 | "ayuDark", 45 | "ayuMirage", 46 | ], 47 | }, 48 | { 49 | label: "Light", 50 | themes: ["githubLight", "solarizedLight", "oneLight", "ayuLight"], 51 | }, 52 | { 53 | label: "Warm", 54 | themes: ["gruvbox"], 55 | }, 56 | { 57 | label: "Cool", 58 | themes: ["nord", "oneDark"], 59 | }, 60 | { 61 | label: "Fun", 62 | themes: ["bubblegum", "rosePine", "cottonCandy", "synthwave", "candyfloss"], 63 | }, 64 | { 65 | label: "Retro", 66 | themes: ["terminal", "amber", "ibmPC"], 67 | }, 68 | { 69 | label: "Accessibility", 70 | themes: ["highContrast"], 71 | }, 72 | ]; 73 | -------------------------------------------------------------------------------- /src/lib/definitions/columns.ts: -------------------------------------------------------------------------------- 1 | import type { Column } from "$lib/types"; 2 | import { formatMemorySize } from "$lib/utils"; 3 | 4 | export let column_definitions: Column[] = [ 5 | { id: "name", label: "Process Name", visible: true, required: true }, 6 | { id: "pid", label: "PID", visible: true, required: false }, 7 | { 8 | id: "status", 9 | label: "Status", 10 | visible: true, 11 | }, 12 | { id: "user", label: "User", visible: true }, 13 | { 14 | id: "cpu_usage", 15 | label: "CPU %", 16 | visible: true, 17 | format: (v) => v.toFixed(1) + "%", 18 | }, 19 | { 20 | id: "memory_usage", 21 | label: "RAM", 22 | visible: true, 23 | format: (v) => (v / (1024 * 1024)).toFixed(1) + " MB", 24 | }, 25 | { 26 | id: "virtual_memory", 27 | label: "VIRT", 28 | visible: true, 29 | format: (v) => formatMemorySize(v), 30 | }, 31 | { 32 | id: "disk_usage", 33 | label: "Disk I/O (R/W)", 34 | visible: true, 35 | format: (v) => { 36 | const readMB = (v[0] / (1024 * 1024)).toFixed(1); 37 | const writeMB = (v[1] / (1024 * 1024)).toFixed(1); 38 | return `${readMB}/${writeMB} MB`; 39 | }, 40 | }, 41 | { id: "ppid", label: "Parent PID", visible: false }, 42 | { id: "root", label: "Root", visible: false }, 43 | { id: "command", label: "Command", visible: false }, 44 | { id: "environ", label: "Environment Variables", visible: false }, 45 | { id: "session_id", label: "Session ID", visible: false }, 46 | { 47 | id: "start_time", 48 | label: "Start Time", 49 | visible: false, 50 | format: (v) => new Date(v * 1000).toLocaleString(), // v is the time where the process was started (in seconds) from epoch 51 | }, 52 | { 53 | id: "run_time", 54 | label: "Run Time", 55 | visible: true, 56 | format: (v) => { 57 | const seconds = v; // v is the time the process has been running in seconds 58 | const hours = Math.floor(seconds / 3600); 59 | const minutes = Math.floor((seconds % 3600) / 60); 60 | const remainingSeconds = seconds % 60; 61 | return `${hours}h ${minutes}m ${remainingSeconds}s`; // Format as HH:MM:SS 62 | }, 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /src/lib/definitions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./columns"; 2 | export * from "./settings"; 3 | export * from "./themes"; 4 | -------------------------------------------------------------------------------- /src/lib/definitions/settings.ts: -------------------------------------------------------------------------------- 1 | import type { AppConfig } from "$lib/types"; 2 | 3 | export const DEFAULT_CONFIG: AppConfig = { 4 | appearance: { 5 | columnVisibility: { 6 | name: true, 7 | pid: true, 8 | status: true, 9 | user: true, 10 | cpu_usage: true, 11 | memory_usage: true, 12 | virtual_memory: true, 13 | disk_usage: true, 14 | ppid: false, 15 | root: false, 16 | command: false, 17 | environ: false, 18 | session_id: false, 19 | start_time: false, 20 | run_time: true, 21 | }, 22 | }, 23 | behavior: { 24 | itemsPerPage: 15, 25 | refreshRate: 1000, 26 | defaultStatusFilter: "all", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./processes"; 2 | export * from "./theme"; 3 | export * from "./settings"; 4 | -------------------------------------------------------------------------------- /src/lib/stores/processes.ts: -------------------------------------------------------------------------------- 1 | import { writable, derived } from "svelte/store"; 2 | import type { Process, SystemStats } from "$lib/types"; 3 | import { invoke } from "@tauri-apps/api/core"; 4 | 5 | interface ProcessStore { 6 | processes: Process[]; 7 | systemStats: SystemStats | null; 8 | error: string | null; 9 | isLoading: boolean; 10 | searchTerm: string; 11 | currentPage: number; 12 | pinnedProcesses: Set; 13 | selectedProcess: Process | null; 14 | showInfoModal: boolean; 15 | showConfirmModal: boolean; 16 | processToKill: Process | null; 17 | isKilling: boolean; 18 | isFrozen: boolean; 19 | selectedProcessPid: number | null; 20 | sortConfig: { 21 | field: keyof Process; 22 | direction: "asc" | "desc"; 23 | }; 24 | } 25 | 26 | const initialState: ProcessStore = { 27 | processes: [], 28 | systemStats: null, 29 | error: null, 30 | isLoading: true, 31 | searchTerm: "", 32 | currentPage: 1, 33 | pinnedProcesses: new Set(), 34 | selectedProcess: null, 35 | showInfoModal: false, 36 | showConfirmModal: false, 37 | processToKill: null, 38 | isKilling: false, 39 | isFrozen: false, 40 | selectedProcessPid: null, 41 | sortConfig: { 42 | field: "cpu_usage", 43 | direction: "desc", 44 | }, 45 | }; 46 | 47 | function createProcessStore() { 48 | const { subscribe, set, update } = writable(initialState); 49 | 50 | // Define all methods first 51 | const setIsLoading = (isLoading: boolean) => 52 | update((state) => ({ ...state, isLoading })); 53 | 54 | const getProcesses = async () => { 55 | try { 56 | const result = await invoke<[Process[], SystemStats]>("get_processes"); 57 | update((state) => { 58 | let updatedSelectedProcess = state.selectedProcess; 59 | if (state.selectedProcessPid) { 60 | updatedSelectedProcess = 61 | result[0].find((p) => p.pid === state.selectedProcessPid) || null; 62 | } 63 | 64 | return { 65 | ...state, 66 | processes: result[0], 67 | systemStats: result[1], 68 | error: null, 69 | selectedProcess: updatedSelectedProcess, 70 | }; 71 | }); 72 | } catch (e: unknown) { 73 | update((state) => ({ 74 | ...state, 75 | error: e instanceof Error ? e.message : String(e), 76 | })); 77 | } 78 | }; 79 | 80 | const killProcess = async (pid: number) => { 81 | try { 82 | update((state) => ({ ...state, isKilling: true })); 83 | const success = await invoke("kill_process", { pid }); 84 | if (success) { 85 | await getProcesses(); 86 | } else { 87 | throw new Error("Failed to kill process"); 88 | } 89 | } catch (e: unknown) { 90 | update((state) => ({ 91 | ...state, 92 | error: e instanceof Error ? e.message : String(e), 93 | })); 94 | } finally { 95 | update((state) => ({ ...state, isKilling: false })); 96 | } 97 | }; 98 | 99 | const toggleSort = (field: keyof Process) => { 100 | update((state) => ({ 101 | ...state, 102 | sortConfig: { 103 | field, 104 | direction: 105 | state.sortConfig.field === field 106 | ? state.sortConfig.direction === "asc" 107 | ? "desc" 108 | : "asc" 109 | : "desc", 110 | }, 111 | })); 112 | }; 113 | 114 | const togglePin = (command: string) => { 115 | update((state) => { 116 | const newPinnedProcesses = new Set(state.pinnedProcesses); 117 | if (newPinnedProcesses.has(command)) { 118 | newPinnedProcesses.delete(command); 119 | } else { 120 | newPinnedProcesses.add(command); 121 | } 122 | return { ...state, pinnedProcesses: newPinnedProcesses }; 123 | }); 124 | }; 125 | 126 | const setSearchTerm = (searchTerm: string) => 127 | update((state) => ({ ...state, searchTerm, currentPage: 1 })); 128 | 129 | const setIsFrozen = (isFrozen: boolean) => 130 | update((state) => ({ ...state, isFrozen })); 131 | 132 | const setCurrentPage = (currentPage: number) => 133 | update((state) => ({ ...state, currentPage })); 134 | 135 | const showProcessDetails = (process: Process) => { 136 | update((state) => ({ 137 | ...state, 138 | selectedProcessPid: process.pid, 139 | selectedProcess: process, 140 | showInfoModal: true, 141 | })); 142 | }; 143 | 144 | const closeProcessDetails = () => { 145 | update((state) => ({ 146 | ...state, 147 | showInfoModal: false, 148 | selectedProcess: null, 149 | selectedProcessPid: null, 150 | })); 151 | }; 152 | 153 | const confirmKillProcess = (process: Process) => { 154 | update((state) => ({ 155 | ...state, 156 | processToKill: process, 157 | showConfirmModal: true, 158 | })); 159 | }; 160 | 161 | const closeConfirmKill = () => { 162 | update((state) => ({ 163 | ...state, 164 | showConfirmModal: false, 165 | processToKill: null, 166 | })); 167 | }; 168 | 169 | const handleConfirmKill = async () => { 170 | let processToKill: Process | null = null; 171 | 172 | let currentState: ProcessStore | undefined; 173 | const unsubscribe = subscribe((state) => { 174 | currentState = state; 175 | }); 176 | unsubscribe(); 177 | 178 | if (currentState?.processToKill && "pid" in currentState.processToKill) { 179 | processToKill = currentState.processToKill; 180 | } 181 | 182 | if (!processToKill?.pid) { 183 | return; 184 | } 185 | 186 | try { 187 | await killProcess(processToKill.pid); 188 | } finally { 189 | update((state) => ({ 190 | ...state, 191 | showConfirmModal: false, 192 | processToKill: null, 193 | })); 194 | } 195 | }; 196 | 197 | // Return all methods 198 | return { 199 | subscribe, 200 | set, 201 | update, 202 | setIsLoading, 203 | getProcesses, 204 | killProcess, 205 | toggleSort, 206 | togglePin, 207 | setSearchTerm, 208 | setIsFrozen, 209 | setCurrentPage, 210 | showProcessDetails, 211 | closeProcessDetails, 212 | confirmKillProcess, 213 | closeConfirmKill, 214 | handleConfirmKill, 215 | }; 216 | } 217 | 218 | export const processStore = createProcessStore(); 219 | -------------------------------------------------------------------------------- /src/lib/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | import type { AppConfig } from "$lib/types"; 3 | import { DEFAULT_CONFIG } from "$lib/definitions/settings"; 4 | 5 | function createSettingsStore() { 6 | const { subscribe, set, update } = writable(DEFAULT_CONFIG); 7 | 8 | return { 9 | subscribe, 10 | init: () => { 11 | if (typeof window !== "undefined") { 12 | const stored = localStorage.getItem("neohtop_config"); 13 | if (stored) { 14 | try { 15 | const config = JSON.parse(stored); 16 | set({ ...DEFAULT_CONFIG, ...config }); 17 | } catch (e) { 18 | console.error("Failed to parse stored config:", e); 19 | set(DEFAULT_CONFIG); 20 | } 21 | } 22 | } 23 | }, 24 | updateConfig: (newConfig: Partial) => { 25 | update((config) => { 26 | const updated = { ...config, ...newConfig }; 27 | if (typeof window !== "undefined") { 28 | localStorage.setItem("neohtop_config", JSON.stringify(updated)); 29 | } 30 | return updated; 31 | }); 32 | }, 33 | }; 34 | } 35 | 36 | export const settingsStore = createSettingsStore(); 37 | -------------------------------------------------------------------------------- /src/lib/stores/theme.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | import { themes } from "$lib/definitions/themes"; 3 | import type { Theme } from "$lib/types"; 4 | function createThemeStore() { 5 | // Default theme 6 | const defaultTheme = themes.catppuccin; 7 | 8 | // Initialize with default theme 9 | const { subscribe, set } = writable(defaultTheme); 10 | 11 | // Initialize theme on client-side only 12 | if (typeof window !== "undefined") { 13 | const storedTheme = localStorage.getItem("theme"); 14 | if (storedTheme && themes[storedTheme]) { 15 | set(themes[storedTheme]); 16 | } 17 | } 18 | 19 | return { 20 | subscribe, 21 | setTheme: (themeName: string) => { 22 | const theme = themes[themeName]; 23 | if (theme) { 24 | if (typeof window !== "undefined") { 25 | localStorage.setItem("theme", themeName); 26 | // Add this line to set the data-theme attribute 27 | document.documentElement.setAttribute("data-theme", themeName); 28 | } 29 | set(theme); 30 | applyTheme(theme); 31 | } 32 | }, 33 | init: () => { 34 | const storedTheme = 35 | typeof window !== "undefined" ? localStorage.getItem("theme") : null; 36 | const theme = (storedTheme && themes[storedTheme]) || defaultTheme; 37 | if (typeof window !== "undefined") { 38 | // Add this line to set the data-theme attribute on init 39 | document.documentElement.setAttribute( 40 | "data-theme", 41 | storedTheme || "catppuccin", 42 | ); 43 | } 44 | applyTheme(theme); 45 | }, 46 | }; 47 | } 48 | 49 | function applyTheme(theme: Theme) { 50 | if (typeof window !== "undefined") { 51 | const root = document.documentElement; 52 | Object.entries(theme.colors).forEach(([key, value]) => { 53 | root.style.setProperty(`--${key}`, value); 54 | }); 55 | } 56 | } 57 | 58 | export const themeStore = createThemeStore(); 59 | -------------------------------------------------------------------------------- /src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | // Create a new types file to centralize interfaces 2 | export interface Process { 3 | pid: number; 4 | ppid: number; 5 | name: string; 6 | cpu_usage: number; 7 | memory_usage: number; 8 | status: string; 9 | user: string; 10 | command: string; 11 | threads?: number; 12 | environ: string[]; 13 | root: string; 14 | virtual_memory: number; 15 | start_time: number; 16 | run_time: number; 17 | disk_usage: [number, number]; // [read_bytes, written_bytes] 18 | session_id?: number; 19 | } 20 | 21 | export interface SystemStats { 22 | cpu_usage: number[]; 23 | memory_total: number; 24 | memory_used: number; 25 | memory_free: number; 26 | memory_cached: number; 27 | uptime: number; 28 | load_avg: [number, number, number]; 29 | network_rx_bytes: number; 30 | network_tx_bytes: number; 31 | disk_total_bytes: number; 32 | disk_used_bytes: number; 33 | disk_free_bytes: number; 34 | } 35 | 36 | export interface Column { 37 | id: keyof Process; 38 | label: string; 39 | visible: boolean; 40 | required?: boolean; 41 | format?: (value: any) => string; 42 | } 43 | 44 | export interface Theme { 45 | name: string; 46 | label: string; 47 | colors: { 48 | base: string; 49 | mantle: string; 50 | crust: string; 51 | text: string; 52 | subtext0: string; 53 | subtext1: string; 54 | surface0: string; 55 | surface1: string; 56 | surface2: string; 57 | overlay0: string; 58 | overlay1: string; 59 | blue: string; 60 | lavender: string; 61 | sapphire: string; 62 | sky: string; 63 | red: string; 64 | maroon: string; 65 | peach: string; 66 | yellow: string; 67 | green: string; 68 | teal: string; 69 | }; 70 | } 71 | 72 | export interface AppConfig { 73 | appearance: { 74 | columnVisibility: Record; 75 | }; 76 | behavior: { 77 | itemsPerPage: number; 78 | refreshRate: number; 79 | defaultStatusFilter: string; 80 | }; 81 | } 82 | 83 | export interface ColumnDefinition { 84 | id: string; 85 | label: string; 86 | visible: boolean; 87 | required?: boolean; 88 | } 89 | 90 | export interface StatusOption { 91 | value: string; 92 | label: string; 93 | } 94 | 95 | export interface RefreshRateOption { 96 | value: number; 97 | label: string; 98 | } 99 | 100 | export interface ToolBarProps { 101 | searchTerm: string; 102 | statusFilter: string; 103 | itemsPerPage: number; 104 | currentPage: number; 105 | totalPages: number; 106 | totalResults: number; 107 | columns: ColumnDefinition[]; 108 | refreshRate: number; 109 | isFrozen: boolean; 110 | } 111 | 112 | export interface SortConfig { 113 | field: keyof Process; 114 | direction: "asc" | "desc"; 115 | } 116 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { Process } from "$lib/types"; 2 | import type { SortConfig } from "$lib/types"; 3 | 4 | export interface ProcessStatus { 5 | label: string; 6 | emoji: string; 7 | color: string; 8 | } 9 | 10 | export function formatMemorySize(bytes: number): string { 11 | const gb = bytes / (1024 * 1024 * 1024); 12 | return `${gb.toFixed(1)} GB`; 13 | } 14 | 15 | export function formatPercentage(value: number): string { 16 | return `${value.toFixed(1)}%`; 17 | } 18 | 19 | export function formatUptime(seconds: number): string { 20 | const days = Math.floor(seconds / 86400); 21 | const hours = Math.floor((seconds % 86400) / 3600); 22 | const minutes = Math.floor((seconds % 3600) / 60); 23 | return `${days}d ${hours}h ${minutes}m`; 24 | } 25 | 26 | export function getUsageClass(percentage: number): string { 27 | if (percentage >= 90) return "critical"; 28 | if (percentage >= 60) return "high"; 29 | if (percentage >= 30) return "medium"; 30 | return "low"; 31 | } 32 | 33 | export function formatBytes(bytes: number): string { 34 | const units = ["B", "KB", "MB", "GB", "TB"]; 35 | let value = bytes; 36 | let unitIndex = 0; 37 | 38 | while (value >= 1024 && unitIndex < units.length - 1) { 39 | value /= 1024; 40 | unitIndex++; 41 | } 42 | 43 | return `${value.toFixed(1)} ${units[unitIndex]}`; 44 | } 45 | 46 | export function formatDate(timestamp: number) { 47 | return new Date(timestamp * 1000).toLocaleString(); 48 | } 49 | 50 | // Cache for compiled regex patterns 51 | const regexCache = new Map(); 52 | 53 | export function filterProcesses( 54 | processes: Process[], 55 | searchTerm: string, 56 | statusFilter: string, 57 | ): Process[] { 58 | // Early return for empty search and "all" status 59 | if (searchTerm.length === 0 && statusFilter === "all") { 60 | return processes; 61 | } 62 | 63 | // Pre-process search terms once 64 | const terms = 65 | searchTerm.length > 0 66 | ? searchTerm.split(",").map((term) => term.trim()) 67 | : []; 68 | 69 | return processes.filter((process) => { 70 | // Early status check 71 | if ( 72 | statusFilter !== "all" && 73 | process.status.toLowerCase() !== statusFilter 74 | ) { 75 | return false; 76 | } 77 | 78 | // Skip search if no terms 79 | if (terms.length === 0) { 80 | return true; 81 | } 82 | 83 | // Cache lowercase values 84 | const processNameLower = process.name.toLowerCase(); 85 | const processCommandLower = process.command.toLowerCase(); 86 | const processPidString = process.pid.toString(); 87 | 88 | // Check each term 89 | return terms.some((term) => { 90 | const termLower = term.toLowerCase(); 91 | 92 | // Try exact matches first (faster) 93 | if ( 94 | processNameLower.includes(termLower) || 95 | processCommandLower.includes(termLower) || 96 | processPidString.includes(term) 97 | ) { 98 | return true; 99 | } 100 | 101 | // Try regex match last (slower) 102 | try { 103 | let regex = regexCache.get(term); 104 | if (!regex) { 105 | regex = new RegExp(term, "i"); 106 | regexCache.set(term, regex); 107 | } 108 | return regex.test(process.name); 109 | } catch { 110 | return false; 111 | } 112 | }); 113 | }); 114 | } 115 | 116 | // Create a Map for quick pinned status lookup 117 | const isPinned = new Map(); 118 | 119 | export function sortProcesses( 120 | processes: Process[], 121 | sortConfig: SortConfig, 122 | pinnedProcesses: Set, 123 | ): Process[] { 124 | // Clear the cache before sorting 125 | isPinned.clear(); 126 | 127 | return [...processes].sort((a, b) => { 128 | // Cache pinned status 129 | let aPin = pinnedProcesses.has(a.command); 130 | isPinned.set(a.command, aPin); 131 | 132 | let bPin = pinnedProcesses.has(b.command); 133 | isPinned.set(b.command, bPin); 134 | 135 | // Quick pin comparison 136 | if (aPin !== bPin) { 137 | return aPin ? -1 : 1; 138 | } 139 | 140 | // Only compute direction once 141 | const direction = sortConfig.direction === "asc" ? 1 : -1; 142 | const aValue = a[sortConfig.field]; 143 | const bValue = b[sortConfig.field]; 144 | 145 | // Special handling for disk_usage which is an array [read_bytes, written_bytes] 146 | if (sortConfig.field === "disk_usage") { 147 | const aRead = (aValue as [number, number])[0]; 148 | const aWrite = (aValue as [number, number])[1]; 149 | const bRead = (bValue as [number, number])[0]; 150 | const bWrite = (bValue as [number, number])[1]; 151 | 152 | // Smart sorting: analyze if this is a read-heavy or write-heavy comparison 153 | const totalReads = aRead + bRead; 154 | const totalWrites = aWrite + bWrite; 155 | 156 | if (totalWrites > totalReads * 1.5) { 157 | // Write-heavy scenario: prioritize writes, use reads as tiebreaker 158 | if (aWrite !== bWrite) { 159 | return direction * (aWrite - bWrite); 160 | } 161 | return direction * (aRead - bRead); 162 | } else if (totalReads > totalWrites * 1.5) { 163 | // Read-heavy scenario: prioritize reads, use writes as tiebreaker 164 | if (aRead !== bRead) { 165 | return direction * (aRead - bRead); 166 | } 167 | return direction * (aWrite - bWrite); 168 | } else { 169 | // Balanced I/O: sort by total, use max as tiebreaker 170 | const aTotalDisk = aRead + aWrite; 171 | const bTotalDisk = bRead + bWrite; 172 | if (aTotalDisk !== bTotalDisk) { 173 | return direction * (aTotalDisk - bTotalDisk); 174 | } 175 | // Tiebreaker: use the dominant operation 176 | const aMaxDisk = Math.max(aRead, aWrite); 177 | const bMaxDisk = Math.max(bRead, bWrite); 178 | return direction * (aMaxDisk - bMaxDisk); 179 | } 180 | } 181 | 182 | // Type-specific comparisons 183 | if (typeof aValue === "string") { 184 | return direction * aValue.localeCompare(bValue as string); 185 | } 186 | return direction * (Number(aValue) - Number(bValue)); 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | // Tauri doesn't have a Node.js server to do proper SSR 2 | // so we will use adapter-static to prerender the app (SSG) 3 | // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info 4 | export const prerender = true; 5 | export const ssr = false; 6 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 90 | 91 | {#if isLoading} 92 |
93 |
94 | 95 |
96 |
97 | {:else} 98 |
99 | 100 |
101 | {#if systemStats} 102 | 103 | {/if} 104 | 105 | 116 | 117 | {#if error} 118 |
{error}
119 | {/if} 120 | 121 | 132 |
133 |
134 | {/if} 135 | 136 | 143 | 144 | 151 | 152 | 252 | -------------------------------------------------------------------------------- /static/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/static/128x128.png -------------------------------------------------------------------------------- /static/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/static/32x32.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdenasser/neohtop/3ea16752d6e79b00c7d505e55257727f3814dcc0/static/favicon.png -------------------------------------------------------------------------------- /static/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /static/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | // Tauri doesn't have a Node.js server to do proper SSR 2 | // so we will use adapter-static to prerender the app (SSG) 3 | // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info 4 | import adapter from "@sveltejs/adapter-static"; 5 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 6 | 7 | /** @type {import('@sveltejs/kit').Config} */ 8 | const config = { 9 | kit: { 10 | adapter: adapter(), 11 | alias: { 12 | $lib: 'src/lib' 13 | } 14 | }, 15 | preprocess: vitePreprocess() 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | 4 | const host = process.env.TAURI_DEV_HOST; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig(async () => ({ 8 | plugins: [sveltekit()], 9 | resolve: { 10 | alias: { 11 | $lib: "/src/lib", 12 | }, 13 | }, 14 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 15 | // 16 | // 1. prevent vite from obscuring rust errors 17 | clearScreen: false, 18 | // 2. tauri expects a fixed port, fail if that port is not available 19 | server: { 20 | port: 1420, 21 | strictPort: true, 22 | host: host || false, 23 | hmr: host 24 | ? { 25 | protocol: "ws", 26 | host, 27 | port: 1421, 28 | } 29 | : undefined, 30 | watch: { 31 | // 3. tell vite to ignore watching `src-tauri` 32 | ignored: ["**/src-tauri/**"], 33 | }, 34 | }, 35 | })); 36 | --------------------------------------------------------------------------------