Showing preview only (300K chars total). Download the full file or copy to clipboard to get everything.
Repository: Abdenasser/neohtop
Branch: main
Commit: dc22a9f475b9
Files: 95
Total size: 276.9 KB
Directory structure:
gitextract_7hzxi6z1/
├── .github/
│ ├── CODEOWNERS
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── 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
├── docs/
│ ├── index.html
│ ├── main.js
│ └── styles.css
├── jsconfig.json
├── package.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
│ │ │ ├── FilterToggle.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
│ │ │ ├── overlay.ts
│ │ │ ├── processes.ts
│ │ │ ├── settings.ts
│ │ │ └── theme.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── utils/
│ │ └── index.ts
│ └── routes/
│ ├── +layout.js
│ ├── +layout.svelte
│ └── +page.svelte
├── src-tauri/
│ ├── .cargo/
│ │ └── config.toml
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── build.rs
│ ├── capabilities/
│ │ └── default.json
│ ├── icons/
│ │ └── icon.icns
│ ├── 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
├── svelte.config.js
└── vite.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
* @Abdenasser
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing to NeoHtop
Thank you for considering contributing to NeoHtop! We welcome contributions from the community.
## How to Contribute
1. Fork the repository.
2. Create a new branch (`git checkout -b feature/YourFeature`).
3. Make your changes.
4. Commit your changes (`git commit -m 'Add some feature'`).
5. Push to the branch (`git push origin feature/YourFeature`).
6. Open a pull request.
## Code of Conduct
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.
## Reporting Bugs
Please use the [bug report template](./ISSUE_TEMPLATE/bug_report.md) to report any bugs you find.
## Requesting Features
Please use the [feature request template](./ISSUE_TEMPLATE/feature_request.md) to suggest new features.
================================================
FILE: .github/FUNDING.yml
================================================
github: abdenasser
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. macOS]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Support
url: https://github.com/Abdenasser/neohtop/discussions
about: Please use discussions for questions and support.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/pull_request_template.md
================================================
## Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
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.
- [ ] Test A
- [ ] Test B
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
================================================
FILE: .github/workflows/build-check.yml
================================================
name: Build Check
on:
pull_request:
branches: [main]
paths:
- "src-tauri/**"
- ".github/workflows/**"
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 1
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
RUSTC_WRAPPER: sccache
CARGO_BUILD_JOBS: 2
jobs:
build:
name: Build Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "npm"
- name: Cache Linux Dependencies
id: cache-apt
uses: actions/cache@v3
with:
path: |
/var/cache/apt/archives/*.deb
/var/lib/apt/lists/*
key: ${{ runner.os }}-apt-${{ hashFiles('**/package.json', '**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-apt-
- name: Add Ubuntu Jammy repo for WebKitGTK 4.0
run: |
echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list
sudo apt update
- name: Install Linux Dependencies
run: |
sudo rm -rf /var/cache/apt/archives/lock
sudo rm -rf /var/cache/apt/archives/partial
sudo rm -rf /var/lib/apt/lists/lock
sudo rm -rf /var/lib/apt/lists/partial
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
build-essential \
pkg-config \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libglib2.0-dev \
libjavascriptcoregtk-4.0-dev \
libsoup-3.0-dev \
libwebkit2gtk-4.1-dev
- name: Remove Jammy repo
run: |
sudo sed -i '/jammy main universe/d' /etc/apt/sources.list
sudo apt update
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: cargo
target: x86_64-unknown-linux-gnu
- name: Install sccache
run: |
SCCACHE_VERSION=v0.7.7
curl -L "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" | tar xz
sudo mv sccache-*/sccache /usr/local/bin/sccache
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src-tauri -> target"
shared-key: "build"
- name: Install Dependencies
run: npm ci
- name: Build Application
run: |
npm run tauri build -- \
--target x86_64-unknown-linux-gnu \
--bundles deb \
--ci
================================================
FILE: .github/workflows/format-check.yml
================================================
name: Format Check
on:
pull_request:
branches: [ main ]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npm run format:check
================================================
FILE: .github/workflows/linux-aarch64-nightly.yml
================================================
name: Linux (aarch64) Nightly Build
on:
workflow_dispatch:
inputs:
release_upload_url:
description: "Release upload URL"
required: true
env:
CARGO_TERM_COLOR: always
PKG_CONFIG_ALLOW_CROSS: 1
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
PKG_CONFIG: /usr/bin/aarch64-linux-gnu-pkg-config
jobs:
build:
name: Build Linux aarch64 Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "npm"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
- name: Configure ARM64 repositories
run: |
sudo dpkg --add-architecture arm64
# Remove all existing sources
sudo rm -rf /etc/apt/sources.list.d/*
sudo truncate -s 0 /etc/apt/sources.list
# Add only ports.ubuntu.com repository
sudo tee /etc/apt/sources.list << EOF
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse
deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu jammy main restricted universe multiverse
deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse
deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu jammy-security main restricted universe multiverse
EOF
sudo apt-get update
# Install required packages including cross-compilation tools
sudo apt-get install -y \
build-essential \
pkg-config \
crossbuild-essential-arm64 \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
libgtk-3-dev:arm64 \
libayatana-appindicator3-dev:arm64 \
librsvg2-dev:arm64 \
libglib2.0-dev:arm64 \
libjavascriptcoregtk-4.0-dev:arm64 \
libsoup-3.0-dev:arm64 \
libwebkit2gtk-4.1-dev:arm64 \
libssl-dev:arm64 \
libssl-dev \
openssl:arm64
# Configure pkg-config for cross-compilation
echo "PKG_CONFIG=/usr/bin/aarch64-linux-gnu-pkg-config" >> $GITHUB_ENV
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
- name: Install Dependencies
run: npm install
- name: Setup cross-compilation environment
run: |
sudo apt-get install -y \
crossbuild-essential-arm64 \
pkg-config \
libssl-dev:arm64 \
libssl-dev \
openssl:arm64 \
file \
desktop-file-utils \
libfuse2 \
qemu-user-static
# Setup pkg-config
sudo tee /usr/bin/aarch64-linux-gnu-pkg-config << 'EOF'
#!/bin/sh
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
exec pkg-config "$@"
EOF
sudo chmod +x /usr/bin/aarch64-linux-gnu-pkg-config
# Create .cargo/config
mkdir -p .cargo
cat > .cargo/config << EOF
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
ar = "aarch64-linux-gnu-ar"
EOF
# Download and setup appimagetool for ARM64
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage
chmod +x appimagetool-aarch64.AppImage
sudo mv appimagetool-aarch64.AppImage /usr/local/bin/appimagetool
# Set environment variables
echo "PKG_CONFIG=/usr/bin/aarch64-linux-gnu-pkg-config" >> $GITHUB_ENV
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=/usr/include/aarch64-linux-gnu" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=/usr/lib/aarch64-linux-gnu" >> $GITHUB_ENV
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
echo "APPIMAGE_EXTRACT_AND_RUN=1" >> $GITHUB_ENV
- name: Build Frontend
run: npm run build
- name: Build AppImage
run: |
echo "Building AppImage for aarch64..."
npm run tauri build -- --target aarch64-unknown-linux-gnu --bundles appimage
cd src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/appimage/
for f in *.AppImage; do
echo "AARCH64_APPIMAGE_PATH=src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/appimage/$f" >> $GITHUB_ENV
done
- name: Build Debian Package
run: |
echo "Building Debian package for aarch64..."
npm run tauri build -- --target aarch64-unknown-linux-gnu --bundles deb
cd src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/
for f in *.deb; do
echo "AARCH64_DEB_PATH=src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/$f" >> $GITHUB_ENV
done
- name: Build RPM Package
run: |
echo "Building RPM package for aarch64..."
npm run tauri build -- --target aarch64-unknown-linux-gnu --bundles rpm
cd src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/rpm/
for f in *.rpm; do
echo "AARCH64_RPM_PATH=src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/rpm/$f" >> $GITHUB_ENV
done
- name: Get version from package.json
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.AARCH64_APPIMAGE_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_aarch64.AppImage
asset_content_type: application/x-executable
- name: Upload Debian Package to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.AARCH64_DEB_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_aarch64.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload RPM Package to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.AARCH64_RPM_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_aarch64.rpm
asset_content_type: application/x-rpm
================================================
FILE: .github/workflows/linux-x86_64-nightly.yml
================================================
name: Linux (x86_64) Nightly Build
on:
workflow_dispatch:
inputs:
release_upload_url:
description: "Release upload URL"
required: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build Linux Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "npm"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Add Ubuntu Jammy repo for WebKitGTK 4.0
run: |
echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list
sudo apt update
- name: Install Linux Dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
pkg-config \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libglib2.0-dev \
libjavascriptcoregtk-4.0-dev \
libsoup-3.0-dev \
libwebkit2gtk-4.1-dev
- name: Remove Jammy repo
run: |
sudo sed -i '/jammy main universe/d' /etc/apt/sources.list
sudo apt update
- name: Install Dependencies
run: |
npm install
- name: Build Frontend
run: npm run build
- name: Build AppImage (x86_64)
run: |
echo "Building AppImage for x86_64..."
npm run tauri build -- --target x86_64-unknown-linux-gnu --bundles appimage
cd src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/
for f in *.AppImage; do
echo "APPIMAGE_PATH=src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/$f" >> $GITHUB_ENV
done
- name: Build Debian Package (x86_64)
run: |
echo "Building Debian package for x86_64..."
npm run tauri build -- --target x86_64-unknown-linux-gnu --bundles deb
cd src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/
for f in *.deb; do
echo "DEB_PATH=src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/$f" >> $GITHUB_ENV
done
- name: Build RPM Package (x86_64)
run: |
echo "Building RPM package for x86_64..."
npm run tauri build -- --target x86_64-unknown-linux-gnu --bundles rpm
cd src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/
for f in *.rpm; do
echo "RPM_PATH=src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/$f" >> $GITHUB_ENV
done
- name: Get version from package.json
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.APPIMAGE_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_x86_64.AppImage
asset_content_type: application/x-executable
- name: Upload Debian Package to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.DEB_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_x86_64.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload RPM Package to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.RPM_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_x86_64.rpm
asset_content_type: application/x-rpm
================================================
FILE: .github/workflows/macos-nightly.yml
================================================
name: MacOS (Intel/Apple Silicon) Nightly Build
on:
workflow_dispatch:
inputs:
release_upload_url:
description: 'Release upload URL'
required: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build MacOS Apps
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Dependencies
run: |
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
npm install
- name: Set up keychain
run: |
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/certificate.p12
security import /tmp/certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
- name: Build Frontend
run: npm run build
- name: Build for Intel Mac
run: |
echo "Building for Intel Mac..."
npm run tauri build -- --target x86_64-apple-darwin --bundles dmg --config "{\"bundle\":{\"macOS\":{\"signingIdentity\": \"Developer ID Application: Abdenasser Elidrissi (785JV74B9Y)\"}}}"
# Rename the Intel build and store the filename
cd src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/
for f in *.dmg; do
mv "$f" "intel-$f"
echo "INTEL_DMG_PATH=src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/intel-$f" >> $GITHUB_ENV
done
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Build for Apple Silicon
run: |
echo "Building for aarch64..."
npm run tauri build -- --target aarch64-apple-darwin --bundles dmg --config "{\"bundle\":{\"macOS\":{\"signingIdentity\": \"Developer ID Application: Abdenasser Elidrissi (785JV74B9Y)\"}}}"
# Rename the Apple Silicon build and store the filename
cd src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/
for f in *.dmg; do
mv "$f" "silicon-$f"
echo "SILICON_DMG_PATH=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/silicon-$f" >> $GITHUB_ENV
done
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Get version from package.json
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload Intel Build to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.INTEL_DMG_PATH }}
asset_name: intel-NeoHtop_${{ steps.version.outputs.version }}_x64.dmg
asset_content_type: application/x-apple-diskimage
- name: Upload Silicon Build to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.SILICON_DMG_PATH }}
asset_name: silicon-NeoHtop_${{ steps.version.outputs.version }}_aarch64.dmg
asset_content_type: application/x-apple-diskimage
================================================
FILE: .github/workflows/test-release.yml
================================================
name: Test Release Build
on:
workflow_dispatch: # Manual trigger
jobs:
create-draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get version from package.json
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Create Draft Release
id: create_release
uses: softprops/action-gh-release@v1
with:
name: "NeoHtop v${{ steps.version.outputs.version }}"
tag_name: "v${{ steps.version.outputs.version }}"
draft: true
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
- name: Trigger MacOS Build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: macos-nightly.yml
token: ${{ secrets.PAT_TOKEN }}
inputs: '{"release_upload_url": "${{ steps.create_release.outputs.upload_url }}"}'
- name: Trigger Windows Build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: windows-nightly.yml
token: ${{ secrets.PAT_TOKEN }}
inputs: '{"release_upload_url": "${{ steps.create_release.outputs.upload_url }}"}'
- name: Trigger Linux x86_64 Build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: linux-x86_64-nightly.yml
token: ${{ secrets.PAT_TOKEN }}
inputs: '{"release_upload_url": "${{ steps.create_release.outputs.upload_url }}"}'
================================================
FILE: .github/workflows/windows-nightly.yml
================================================
name: Windows (x86_64) Nightly Build
on:
workflow_dispatch:
inputs:
release_upload_url:
description: 'Release upload URL'
required: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build Windows Executable
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install WebView2
run: |
$WebView2InstallPath = "$env:TEMP\MicrosoftEdgeWebview2Setup.exe"
Invoke-WebRequest "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile $WebView2InstallPath
Start-Process -FilePath $WebView2InstallPath -Args "/silent /install" -Wait
- name: Install Dependencies
run: |
npm install
- name: Build Frontend
run: npm run build
- name: Build Windows Executable
shell: bash # Force using bash shell for consistent environment variable setting
run: |
echo "Building Windows executable..."
npm run tauri build
echo "WIN_EXE_PATH=src-tauri/target/release/NeoHtop.exe" >> $GITHUB_ENV
- name: Get version from package.json
id: version
shell: bash # Force using bash shell
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload to Release
if: github.event.inputs.release_upload_url != ''
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
with:
upload_url: ${{ github.event.inputs.release_upload_url }}
asset_path: ${{ env.WIN_EXE_PATH }}
asset_name: NeoHtop_${{ steps.version.outputs.version }}_x64.exe
asset_content_type: application/vnd.microsoft.portable-executable
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
## Jetbrains
.idea/
.run/
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm exec lint-staged
================================================
FILE: .prettierrc
================================================
{
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"svelte.enable-ts-plugin": true
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Abdenasser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<div align="center">
<img src="app-icon.png" alt="NeoHtop Logo" width="120" />
<h1>NeoHtop</h1>
<p>A modern, cross-platform system monitor built on top of Svelte, Rust, and Tauri.</p>
[](https://github.com/Abdenasser/neohtop/blob/main/LICENSE)
[](https://github.com/Abdenasser/neohtop/stargazers)
[](https://github.com/Abdenasser/neohtop/issues)
[](https://github.com/Abdenasser/neohtop/releases)
[](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
</div>
<div align="center">
<picture>
<!-- <source media="(prefers-color-scheme: dark)" srcset="screenshot.png">
<source media="(prefers-color-scheme: light)" srcset="screenshot-light.png"> -->
<img alt="NeoHtop Screenshot" src="./screenshot.png" width="800">
</picture>
</div>
<div align="center">
<p>If you find this project helpful, consider buying me a coffee:</p>
<a href="https://www.buymeacoffee.com/abdenasser" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
<p>Or sponsor me on GitHub:</p>
<a href="https://github.com/sponsors/Abdenasser" target="_blank"><img src="https://img.shields.io/badge/Sponsor-abdenasser-white?style=flat&logo=github&logoColor=pink" alt="Sponsor @abdenasser" style="height: auto !important;width: 217px !important;"></a>
</div>
## Table of Contents
- [Why NeoHtop?](#why-neohtop)
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Running with Sudo](#running-with-sudo)
- [Development](#development)
- [Setup](#setup)
- [Code Formatting](#code-formatting)
- [Pull Requests](#pull-requests)
- [Contributing](#contributing)
- [License](#license)
## Why NeoHtop?
[Read about the back story and motivation behind NeoHtop](https://www.abdenasser.com/2024/11/06/oh-boy-neohtop/)
## Features
- 🚀 Real-time process monitoring
- 💻 CPU and Memory usage tracking
- 🎨 Beautiful, modern UI with dark/light themes
- 🔍 Advanced process search and filtering
- 📌 Pin important processes
- 🛠 Process management (kill processes)
- 🎯 Sort by any column
- 🔄 Auto-refresh system stats
### Search Functionality
Search for processes by name, command, or PID. Use commas to search for multiple terms simultaneously. Regular expressions are supported for advanced filtering.
Examples:
- `arm, x86`: Returns processes with "arm" or "x86" in the name or command
- `d$`: Lists daemons (processes ending with 'd')
- `^(\w+\.)+\w+$`: Shows processes with reverse domain name notation (e.g., com.docker.vmnetd)
## Tech Stack
- **Frontend**: SvelteKit, TypeScript
- **Backend**: Rust, Tauri
- **Styling**: CSS Variables for theming
- **Icons**: FontAwesome
## Getting Started
### Prerequisites
- Node.js (v16 or later)
- Rust (latest stable)
- Xcode Command Line Tools (for macOS)
### Installation
#### Manual
Download the latest release from the [releases page](https://github.com/Abdenasser/neohtop/releases).
#### Package Managers
Members of the community have kindly published unofficial packages for various platforms and package managers.
Please note, these packages are community-maintained and not officially released, reviewed, or endorsed by NeoHtop.
We only provide official builds through the GitHub Releases page.
Since these external packages are managed by third parties, we cannot guarantee their security, integrity, or update frequency.
Please use them at your own discretion.
##### macOS
Using [Homebrew](https://brew.sh/).
```bash
brew install --cask neohtop
```
##### Arch Linux (AUR)
Using the [AUR](https://aur.archlinux.org/) and [an AUR helper](https://wiki.archlinux.org/title/AUR_helpers).
```bash
yay -S neohtop
```
or
```bash
paru -S neohtop
```
##### Fedora Linux
Install the [Terra repository](https://terra.fyralabs.com/).
```bash
dnf install neohtop
```
##### Windows
Install the [Scoop repository](https://scoop.sh/), then make sure you have the Scoop extras bucket added:
```bash
scoop bucket add extras
```
Then install with:
```bash
scoop install extras/neohtop
```
##### Solus
```bash
eopkg install neohtop
```
### Running with Sudo
Some processes require monitoring with sudo privileges. To monitor these processes, launch NeoHtop with sudo:
- macOS: `sudo /Applications/NeoHtop.app/Contents/MacOS/NeoHtop`
- Linux: `pkexec /path/to/neohtop` (recommended)
## Development
### Setup
```bash
# Install dependencies
npm install
# Run in development mode
npm run tauri dev
# Build for production
npm run tauri build
```
### Code Formatting
We use Prettier for web code and `cargo fmt` for Rust code.
```bash
# Format all files
npm run format
# Check formatting without making changes
npm run format:check
```
### Pull Requests
Before submitting a PR, ensure:
1. All code is formatted (`npm run format`)
2. The format check passes (`npm run format:check`)
3. Your commits follow the project's commit message conventions
## Contributing
We welcome contributions! Please see our [contributing guidelines](./.github/CONTRIBUTING.md) for more information.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
================================================
FILE: docs/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeoHtop - Blazing-fast system monitoring for your desktop (built with Rust, Tauri & Svelte)</title>
<meta name="description"
content="A beautiful, efficient system monitor built with Rust and Svelte. Monitor processes, CPU, and memory usage in real-time with a modern interface.">
<meta name="keywords"
content="system monitor, task manager, cross-platform, process monitor, cpu usage, memory usage, rust, svelte">
<meta name="author" content="Your Name">
<meta property="og:type" content="website">
<meta property="og:url" content="https://abdenasser.github.io/neohtop/">
<meta property="og:title" content="NeoHtop - Blazing-fast system monitoring for your desktop">
<meta property="og:description"
content="A beautiful, efficient system monitor built with Rust and Svelte. Monitor processes, CPU, and memory usage in real-time.">
<meta property="og:image" content="https://github.com/Abdenasser/neohtop/raw/main/app-icon.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="@__abdenasser">
<meta name="twitter:title" content="NeoHtop - Blazing-fast system monitoring for your desktop">
<meta name="twitter:description" content="A beautiful, efficient system monitor built with Rust and Svelte.">
<meta name="twitter:image" content="https://github.com/Abdenasser/neohtop/raw/main/app-icon.png">
<link rel="icon" type="image/png" href="favicon.ico">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<link rel="stylesheet" href="styles.css">
<link rel="preload" href="https://github.com/Abdenasser/neohtop/raw/main/app-icon.png" as="image">
<link rel="preload" href="https://github.com/Abdenasser/neohtop/raw/main/screenshot.png" as="image">
</head>
<body>
<header class="glass-nav">
<nav>
<div class="logo-container">
<img src="https://github.com/Abdenasser/neohtop/raw/main/app-icon.png" alt="NeoHtop" class="nav-logo">
<span class="nav-brand">NeoHtop</span>
</div>
<button class="menu-button" aria-label="Toggle menu">
☰
</button>
<div class="nav-links">
<a href="#home">Home</a>
<a href="#features">Features</a>
<a href="#download">Download</a>
<a href="#install">Install</a>
<a href="#testimonials">Testimonials</a>
<a href="#faq">FAQ</a>
<button id="themeToggle" class="theme-toggle" aria-label="Toggle theme">
<span class="theme-icon">🌙</span>
</button>
</div>
</nav>
</header>
<main>
<section id="home" class="hero">
<div class="hero-content">
<div class="hero-text">
<h1 class="gradient-text">Monitor Your System<br>With Style</h1>
<p class="hero-subtitle">A beautiful, lightning-fast cross-platform system monitor.</p>
<div class="hero-cta">
<a href="#download" class="primary-button">Download Now</a>
<a href="https://github.com/abdenasser/neohtop" class="secondary-button">
View on GitHub
</a>
</div>
<div class="badges">
<script data-name="BMC-Widget" data-cfasync="false"
src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="abdenasser"
data-description="Support me on Buy me a coffee!" data-message="" data-color="#FF5F5F"
data-position="Right" data-x_margin="18" data-y_margin="18"></script>
</div>
</div>
</div>
<div class="hero-background">
<img src="https://github.com/Abdenasser/neohtop/raw/main/screenshot-light.png" alt="NeoHtop Interface light" />
<img src="https://github.com/Abdenasser/neohtop/raw/main/screenshot.png" alt="NeoHtop Interface dark" />
</div>
</section>
<section id="features" class="features">
<h2 class="section-title">Why Choose NeoHtop?</h2>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🚀</div>
<h3>Real-time Monitoring</h3>
<p>Track system processes in real-time with minimal resource usage</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎨</div>
<h3>Modern UI</h3>
<p>Beautiful interface with automatic dark/light theme detection</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔍</div>
<h3>Smart Search</h3>
<p>Quick process search with advanced filtering options</p>
</div>
<div class="feature-card">
<div class="feature-icon">📌</div>
<h3>Process Pinning</h3>
<p>Keep important processes in view for easy monitoring</p>
</div>
<div class="feature-card">
<div class="feature-icon">⚡️</div>
<h3>Resource Efficient</h3>
<p>Built with Rust for optimal performance and low memory usage</p>
</div>
<div class="feature-card">
<div class="feature-icon">🛠</div>
<h3>Process Management</h3>
<p>View and manage processes with detailed information</p>
</div>
</div>
</section>
<section id="download" class="downloads">
<div class="download-container glass-card">
<h2>Download NeoHtop <span id="current-version">Loading...</span></h2>
<div class="download-stats">
<span id="download-count">...</span> Total Downloads
</div>
<p class="download-subtitle">Choose your platform to get started</p>
<div class="download-options">
<!-- macOS Downloads -->
<div class="download-group">
<h3 class="platform-title">macOS</h3>
<!-- notarized by apple badge -->
<div class="notarized-badge">
<img
src="https://img.shields.io/badge/Notarized%20by%20Apple-000000?style=for-the-badge&logo=apple&logoColor=white"
alt="Notarized by Apple">
</div>
<div class="platform-options">
<a href="" class="download-button macos" data-type="macos-intel" data-version="latest">
<span class="icon">💻</span>
<div class="button-text">
<span class="primary">Intel Chip</span>
<span class="secondary">macOS 10.15 or later</span>
</div>
</a>
<a href="" class="download-button macos" data-type="macos-silicon" data-version="latest">
<span class="icon">🍎</span>
<div class="button-text">
<span class="primary">Apple Silicon</span>
<span class="secondary">macOS 11.0 or later</span>
</div>
</a>
</div>
</div>
<!-- Windows Download -->
<div class="download-group">
<h3 class="platform-title">Windows</h3>
<div class="platform-options">
<a href="" class="download-button windows" data-type="windows" data-version="latest">
<span class="icon">🫣</span>
<div class="button-text">
<span class="primary">Windows</span>
<span class="secondary">Windows 10 or later</span>
</div>
</a>
</div>
</div>
<!-- Linux Downloads -->
<div class="download-group">
<h3 class="platform-title">Linux</h3>
<div class="platform-options">
<!-- x86_64 Downloads -->
<div class="linux-downloads">
<h4>x86_64</h4>
<a href="" class="download-button linux" data-type="linux-deb-x64" data-version="latest">
<span class="icon">📦</span>
<div class="button-text">
<span class="primary">.deb Package</span>
<span class="secondary">Debian/Ubuntu</span>
</div>
</a>
<a href="" class="download-button linux" data-type="linux-appimage-x64" data-version="latest"
style="margin-top: 10px;">
<span class="icon">🐧</span>
<div class="button-text">
<span class="primary">AppImage</span>
<span class="secondary">Universal Linux</span>
</div>
</a>
<a href="" class="download-button linux" data-type="linux-rpm-x64" data-version="latest"
style="margin-top: 10px;">
<span class="icon">📦</span>
<div class="button-text">
<span class="primary">.rpm Package</span>
<span class="secondary">Fedora/RHEL x86_64</span>
</div>
</a>
</div>
<!-- ARM64 Downloads -->
<div class="linux-downloads">
<h4>ARM64</h4>
<a href="" class="download-button linux" data-type="linux-deb-arm64" data-version="latest">
<span class="icon">📦</span>
<div class="button-text">
<span class="primary">.deb Package</span>
<span class="secondary">Debian/Ubuntu ARM64</span>
</div>
</a>
<a href="" class="download-button linux" data-type="linux-appimage-arm64" data-version="latest"
style="margin-top: 10px;">
<span class="icon">🐧</span>
<div class="button-text">
<span class="primary">AppImage</span>
<span class="secondary">Universal Linux ARM64</span>
</div>
</a>
<a href="" class="download-button linux" data-type="linux-rpm-arm64" data-version="latest"
style="margin-top: 10px;">
<span class="icon">📦</span>
<div class="button-text">
<span class="primary">.rpm Package</span>
<span class="secondary">Fedora/RHEL ARM64</span>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="install" class="install">
<h2>Installation Guide</h2>
<div class="install-steps">
<div class="step">
<span class="step-number">1</span>
<h3>Download</h3>
<p>Choose and download the appropriate version for your Mac</p>
</div>
<div class="step">
<span class="step-number">2</span>
<h3>Open DMG</h3>
<p>Double-click the downloaded .dmg file</p>
</div>
<div class="step">
<span class="step-number">3</span>
<h3>Install</h3>
<p>Drag NeoHtop to your Applications folder</p>
</div>
<div class="step">
<span class="step-number">4</span>
<h3>First Launch</h3>
<p>Right-click and choose Open to bypass Gatekeeper</p>
</div>
</div>
</section>
<section id="testimonials" class="testimonials">
<h2 class="section-title">What People Are Saying</h2>
<div class="testimonial-grid">
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">Neohtop is absolutely beautiful to look at. It's a modern system monitor inspired by
htop, providing real-time insights into CPU, memory, disk, and network usage. A work of art! <a
href="https://t.co/TAzCIEb8uF">pic.twitter.com/TAzCIEb8uF</a></p>— JoeyBaggaDonuts (@haxxusj) <a
href="https://twitter.com/haxxusj/status/1854590782684250487?ref_src=twsrc%5Etfw">November 7, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="zh" dir="ltr">Htop 加强版: NeoHtop<br>基于 Svelte、Rust 和 Tauri 构建的现代化、跨平台的系统监控工具<a
href="https://t.co/OtWlxxrCHZ">https://t.co/OtWlxxrCHZ</a> <a
href="https://t.co/2MK9n8rZVw">pic.twitter.com/2MK9n8rZVw</a></p>— Geek (@geekbb) <a
href="https://twitter.com/geekbb/status/1854081285846888881?ref_src=twsrc%5Etfw">November 6, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="zh" dir="ltr">💻 NeoHtop:现代跨平台系统监视器!实时追踪 CPU 和内存使用,支持进程管理。<br><br>基于 Svelte、Rust 和 Tauri
构建,轻量高效,非常适合日常系统监控!<br><br>👉 <a href="https://t.co/MaOIMMbbPV">https://t.co/MaOIMMbbPV</a> <a
href="https://t.co/rFLyULJVei">https://t.co/rFLyULJVei</a> <a
href="https://t.co/6c6hGOeiLa">pic.twitter.com/6c6hGOeiLa</a></p>— 小弟调调 (@jaywcjlove) <a
href="https://twitter.com/jaywcjlove/status/1854089502853357880?ref_src=twsrc%5Etfw">November 6, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">neoHTOP: A modern, cross-platform system monitor built on top of Svelte, Rust and
Tauri<a href="https://t.co/MKZ53ZThKv">https://t.co/MKZ53ZThKv</a></p>— Nitin (@gniting) <a
href="https://twitter.com/gniting/status/1853920938976223463?ref_src=twsrc%5Etfw">November 5, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">🚀 Introducing NeoHtop, the ultimate process monitoring tool for all your system needs!
🖥️ Say goodbye to clunky, outdated monitors and hello to a modern, native solution with a clean and
user-friendly interface. 🤩 With NeoHtop, you can easily keep track of your system's… <a
href="https://t.co/qq8cXI8Qq1">pic.twitter.com/qq8cXI8Qq1</a></p>— Durgesh Gupta (@ilearnbydoing) <a
href="https://twitter.com/ilearnbydoing/status/1854037593987428754?ref_src=twsrc%5Etfw">November 6, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="zxx" dir="ltr"><a href="https://t.co/2EoanAOvYL">https://t.co/2EoanAOvYL</a></p>— Tom Dörr
(@tom_doerr) <a href="https://twitter.com/tom_doerr/status/1853861836736881133?ref_src=twsrc%5Etfw">November
5, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="ru" dir="ltr">NeoHtop – мощный и стильный менеджер процессов<a
href="https://t.co/uiGhE2YlFC">https://t.co/uiGhE2YlFC</a></p>— ITforNote (@itfornote) <a
href="https://twitter.com/itfornote/status/1855943550036299934?ref_src=twsrc%5Etfw">November 11, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">Discover NeoHtop, a modern task manager for macOS combining terminal power with a
user-friendly interface, built with Rust, Tauri & Svelte.<br><br>{ author: <a
href="https://twitter.com/__abdenasser?ref_src=twsrc%5Etfw">@__abdenasser</a> } <a
href="https://twitter.com/hashtag/CodeNewbie?src=hash&ref_src=twsrc%5Etfw">#CodeNewbie</a><a
href="https://t.co/rowekkvOUY">https://t.co/rowekkvOUY</a></p>— CodeNewbie (@CodeNewbies) <a
href="https://twitter.com/CodeNewbies/status/1854886849677824003?ref_src=twsrc%5Etfw">November 8, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="de" dir="ltr">NeoHtop: Ein Systemmonitor für macOS <a
href="https://t.co/wUEMeCKpkv">https://t.co/wUEMeCKpkv</a></p>— Ramón Goeden 🤨 (@websenat) <a
href="https://twitter.com/websenat/status/1854134191480262993?ref_src=twsrc%5Etfw">November 6, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="de" dir="ltr">NeoHtop: Ein Systemmonitor für macOS <a
href="https://t.co/RCjYWlC81x">https://t.co/RCjYWlC81x</a></p>— CaschysBlog (@CaschysBlog) <a
href="https://twitter.com/CaschysBlog/status/1854110661502017689?ref_src=twsrc%5Etfw">November 6, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">NeoHtop offers a modern interface with additional features like process pinning, smart
search, and themes while maintaining high performance through its Rust backend. It's designed to be more
user-friendly and efficient than traditional system monitors.<a
href="https://t.co/c4niSeqw0p">https://t.co/c4niSeqw0p</a> <a
href="https://t.co/hnqrfSiSFG">pic.twitter.com/hnqrfSiSFG</a></p>— Hustle Hacker (@HustleHackerAI)
<a href="https://twitter.com/HustleHackerAI/status/1854352028304040139?ref_src=twsrc%5Etfw">November 7,
2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">NeoHtopめちゃかっこいい。macOSでも動く。Apple
Silicon版もちゃんとある。手元で動かしてみたけどアクティビティモニタより好きかも。<br><br>Abdenasser/neohtop: 💪🏻 htop on steroids <a
href="https://t.co/u70aTS1rKK">https://t.co/u70aTS1rKK</a> <a
href="https://t.co/c0iyjiW73q">pic.twitter.com/c0iyjiW73q</a></p>— Isao Shimizu (@isaoshimizu) <a
href="https://twitter.com/isaoshimizu/status/1854097990698586584?ref_src=twsrc%5Etfw">November 6, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">見た目好みで使ってみてるシステムモニター、<a
href="https://t.co/7lHLB8WtVk">https://t.co/7lHLB8WtVk</a><br>トグルボタンのアクティブ状態をアイコンを45度傾けるという方法で示してて、特殊すぎて2度見した
<a href="https://t.co/JFC6ueyfBF">pic.twitter.com/JFC6ueyfBF</a>
</p>— UMERUMA (@umeruma) <a
href="https://twitter.com/umeruma/status/1855209294888902865?ref_src=twsrc%5Etfw">November 9, 2024</a>
</blockquote>
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">🖥️ <a
href="https://twitter.com/hashtag/NeoHtop?src=hash&ref_src=twsrc%5Etfw">#NeoHtop</a>: Cross-platform
system monitor combining <a
href="https://twitter.com/hashtag/Svelte?src=hash&ref_src=twsrc%5Etfw">#Svelte</a>, <a
href="https://twitter.com/hashtag/Rust?src=hash&ref_src=twsrc%5Etfw">#Rust</a>, and <a
href="https://twitter.com/hashtag/Tauri?src=hash&ref_src=twsrc%5Etfw">#Tauri</a> for a modern
monitoring experience <a
href="https://twitter.com/hashtag/opensource?src=hash&ref_src=twsrc%5Etfw">#opensource</a> <a
href="https://twitter.com/hashtag/devops?src=hash&ref_src=twsrc%5Etfw">#devops</a> <br><br>🚀 Key
features:<br>Real-time process monitoring<br>CPU & memory usage tracking<br>Process search &
filtering<br>Dark/light theme support<br><br>💻 Tech stack:…</p>— Micha(el) Bladowski 🇩🇪 🇺🇦
(@michabbb) <a href="https://twitter.com/michabbb/status/1853971838768173409?ref_src=twsrc%5Etfw">November 6,
2024</a>
</blockquote>
</div>
</section>
<section id="faq" class="faq">
<h2 class="section-title">Frequently Asked Questions</h2>
<div class="faq-list">
<div class="faq-item">
<button class="faq-question">
<span class="question-text">How does NeoHtop compare to Activity Monitor?</span>
<span class="faq-icon">⌄</span>
</button>
<div class="faq-answer">
<p>NeoHtop offers a modern interface with additional features like process pinning, smart search, and themes
while maintaining high performance through its Rust backend. It's designed to be more user-friendly and
efficient than traditional system monitors.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span class="question-text">Is NeoHtop resource intensive?</span>
<span class="faq-icon">⌄</span>
</button>
<div class="faq-answer">
<p>No, NeoHtop is built with Rust and optimized for minimal resource usage, typically using less than 1% CPU
and minimal memory. It's designed to be lightweight while monitoring your system.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span class="question-text">Does NeoHtop work on all macOS versions?</span>
<span class="faq-icon">⌄</span>
</button>
<div class="faq-answer">
<p>NeoHtop supports macOS 10.15 (Catalina) and newer versions. It's optimized for both Intel and Apple
Silicon Macs, with native support for both architectures.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span class="question-text">Can I customize the interface?</span>
<span class="faq-icon">⌄</span>
</button>
<div class="faq-answer">
<p>Yes, NeoHtop offers various customization options including:
<ul>
<li>Dark/Light theme switching</li>
<li>Customizable columns and metrics</li>
<li>Adjustable refresh rates</li>
<li>Process grouping options</li>
</ul>
</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span class="question-text">Is NeoHtop open source?</span>
<span class="faq-icon">⌄</span>
</button>
<div class="faq-answer">
<p>Yes, NeoHtop is completely open source and available on GitHub. You can contribute to the project, report
issues, or suggest new features through our GitHub repository.</p>
</div>
</div>
</div>
</section>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-88HFXRBNLS"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-88HFXRBNLS');
</script>
<script src="main.js"></script>
</main>
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h3>NeoHtop</h3>
<p class="footer-description">
A modern system monitor built with Rust and Svelte.
Open source and free to use.
</p>
</div>
<div class="footer-section">
<h4>Links</h4>
<ul class="footer-links">
<li>
<a href="https://github.com/abdenasser/neohtop" target="_blank" rel="noopener">
<span class="icon">📦</span> GitHub Repository
</a>
</li>
<li>
<a href="https://github.com/abdenasser" target="_blank" rel="noopener">
<span class="icon">👨💻</span> Creator
</a>
</li>
<li>
<a href="https://github.com/abdenasser/neohtop/issues" target="_blank" rel="noopener">
<span class="icon">🐛</span> Report Issue
</a>
</li>
</ul>
</div>
<div class="footer-section">
<h4>Tech Stack</h4>
<ul class="tech-stack-list">
<li>
<a href="https://www.rust-lang.org/" target="_blank" rel="noopener">
<span class="icon">🦀</span> Rust
</a>
</li>
<li>
<a href="https://tauri.app/" target="_blank" rel="noopener">
<span class="icon">⚡</span> Tauri
</a>
</li>
<li>
<a href="https://svelte.dev/" target="_blank" rel="noopener">
<span class="icon">🎯</span> Svelte
</a>
</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>Made with <span class="heart">❤️</span> and <span class="coffee">☕</span> by
<a href="https://github.com/abdenasser" target="_blank" rel="noopener">Abdenasser</a>
</p>
</div>
</footer>
</body>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</html>
================================================
FILE: docs/main.js
================================================
// ===============================
// Theme Management
// ===============================
const themeToggle = document.getElementById('themeToggle');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
function setTheme(isDark) {
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
themeToggle.querySelector('.theme-icon').textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme === 'dark');
} else {
setTheme(prefersDark.matches);
}
// Theme event listeners
themeToggle.addEventListener('click', () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
setTheme(!isDark);
});
prefersDark.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
setTheme(e.matches);
}
});
// ===============================
// Version and Download Management
// ===============================
async function fetchDownloadStats() {
try {
const releasesResponse = await fetch('https://api.github.com/repos/abdenasser/neohtop/releases');
const releases = await releasesResponse.json();
const githubDownloads = releases.reduce((total, release) => {
const releaseDownloads = release.assets.reduce((sum, asset) =>
sum + asset.download_count, 0);
return total + releaseDownloads;
}, 0);
const brewResponse = await fetch('https://formulae.brew.sh/api/analytics/install/homebrew-core/365d.json');
const brewData = await brewResponse.json();
const brewInstalls = brewData.formulae?.neohtop?.[0]?.count || 0;
const totalDownloads = githubDownloads + brewInstalls;
document.getElementById('download-count').textContent = new Intl.NumberFormat().format(totalDownloads);
} catch (error) {
console.error('Failed to fetch download stats:', error);
document.getElementById('download-count').textContent = 'N/A';
}
}
async function updateVersion() {
try {
const response = await fetch('https://api.github.com/repos/Abdenasser/neohtop/releases/latest');
const data = await response.json();
const version = data.tag_name;
const versionNumber = version.match(/\d+\.\d+\.\d+/)?.[0];
if (versionNumber) {
document.getElementById('current-version').textContent = "v" + versionNumber;
updateDownloadLinks(versionNumber);
}
} catch (error) {
console.error('Failed to fetch version:', error);
}
}
function updateDownloadLinks(versionNumber) {
const platformUrls = {
'macos-intel': `intel-NeoHtop_${versionNumber}_x64.dmg`,
'macos-silicon': `silicon-NeoHtop_${versionNumber}_aarch64.dmg`,
'windows': `NeoHtop_${versionNumber}_x64.exe`,
'linux-deb-x64': `NeoHtop_${versionNumber}_x86_64.deb`,
'linux-appimage-x64': `NeoHtop_${versionNumber}_x86_64.AppImage`,
'linux-rpm-x64': `NeoHtop_${versionNumber}_x86_64.rpm`,
'linux-deb-arm64': `NeoHtop_${versionNumber}_aarch64.deb`,
'linux-appimage-arm64': `NeoHtop_${versionNumber}_aarch64.AppImage`,
'linux-rpm-arm64': `NeoHtop_${versionNumber}_aarch64.rpm`
};
document.querySelectorAll('.download-button').forEach(link => {
const platform = link.getAttribute('data-type');
if (platformUrls[platform]) {
link.href = `https://github.com/Abdenasser/neohtop/releases/download/v${versionNumber}/${platformUrls[platform]}`;
}
});
}
// ===============================
// UI Interactions
// ===============================
// FAQ Accordion
document.querySelectorAll('.faq-question').forEach(button => {
button.addEventListener('click', () => {
const faqItem = button.parentElement;
const isActive = faqItem.classList.contains('active');
document.querySelectorAll('.faq-item').forEach(item => item.classList.remove('active'));
if (!isActive) faqItem.classList.add('active');
});
});
// Download tracking
document.querySelectorAll('.download-button').forEach(button => {
button.addEventListener('click', (e) => {
gtag('event', 'download', {
'event_category': 'App',
'event_label': button.getAttribute('data-type'),
'value': button.getAttribute('data-version')
});
});
});
// Smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// ===============================
// Animations
// ===============================
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.feature, .step, .download-button').forEach(el => {
observer.observe(el);
});
// ===============================
// Initialization
// ===============================
document.addEventListener('DOMContentLoaded', () => {
updateVersion();
fetchDownloadStats();
});
// ===============================
// Mobile Navigation
// ===============================
document.addEventListener('DOMContentLoaded', () => {
const menuButton = document.querySelector('.menu-button');
const navLinks = document.querySelector('.nav-links');
menuButton.addEventListener('click', () => {
navLinks.classList.toggle('active');
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!navLinks.contains(e.target) && !menuButton.contains(e.target)) {
navLinks.classList.remove('active');
}
});
});
================================================
FILE: docs/styles.css
================================================
:root {
--primary-color: #6366f1;
--secondary-color: #818cf8;
--background: #ffffff;
--text-primary: #1f2937;
--text-secondary: #4b5563;
--card-background: rgba(255, 255, 255, 0.8);
}
[data-theme="dark"] {
--background: #0f172a;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--card-background: rgba(30, 41, 59, 0.8);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--background);
color: var(--text-primary);
line-height: 1.6;
transition: background-color 0.3s, color 0.3s;
margin: 0;
padding: 0;
min-height: 100vh;
width: 100%;
}
/* Header & Navigation */
header {
background: var(--card);
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
}
nav {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-container {
display: flex;
align-items: center;
gap: 0.75rem;
}
.nav-logo {
width: 32px;
/* Adjust size as needed */
height: 32px;
object-fit: contain;
}
.nav-brand {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.nav-links {
display: flex;
align-items: center;
gap: 2rem;
}
.nav-links a {
color: var(--text-primary);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--primary-color);
}
.theme-toggle {
background: transparent;
border: none;
cursor: pointer;
padding: 0.5rem;
color: var(--text-primary);
}
/* Theme Toggle */
.theme-toggle {
background: none;
border: none;
color: var(--text);
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: background-color 0.2s;
}
.theme-toggle:hover {
background: var(--hover);
}
/* Main Content */
main {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
padding-top: 64px;
min-height: 100vh;
display: flex;
flex-direction: column;
width: 100%;
/* Should match the height of your nav */
}
/* Sections */
section {
margin: 6rem 0;
scroll-margin-top: 5rem;
}
/* Hero Section */
.hero {
height: 600px;
width: 100%;
display: flex;
align-items: center;
overflow: hidden;
}
.hero-content {
flex: 0 0 50%;
padding: 2rem;
z-index: 2;
position: relative;
}
.hero-content::before {
content: '';
position: absolute;
top: -50px;
left: -50px;
width: calc(100% + 100px);
height: calc(100% + 100px);
background: linear-gradient(to right,
var(--background) 0%,
var(--background) 60%,
transparent 100%);
z-index: -1;
}
.hero-background {
flex: 0 0 90%;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: -40%;
padding-right: 40%;
}
.hero-background img {
max-width: 200%;
max-height: 200%;
width: auto;
height: 100%;
object-fit: contain;
}
.hero-background img[src*="screenshot.png"] {
display: none;
}
.hero-background img[src*="screenshot-light.png"] {
display: block;
}
/* Dark mode */
[data-theme="dark"] .hero-background img[src*="screenshot.png"] {
display: block;
}
[data-theme="dark"] .hero-background img[src*="screenshot-light.png"] {
display: none;
}
.gradient-text {
font-size: 3rem;
font-weight: 800;
line-height: 1.2;
margin-bottom: 1rem;
}
.hero-subtitle {
font-size: 1.125rem;
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.hero-cta {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.hero-image {
width: 100%;
}
.floating-screenshot {
width: 100%;
height: auto;
max-width: 600px;
border-radius: 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
animation: float 6s ease-in-out infinite;
}
/* Responsive design */
@media (max-width: 1024px) {
.hero {
flex-direction: column;
height: auto;
min-height: auto;
}
.hero-content {
flex: 0 0 100%;
width: 100%;
padding: 3rem 2rem;
text-align: center;
}
.hero-background {
flex: 0 0 100%;
margin-right: 0;
padding-right: 0;
}
.hero-background img {
width: 100%;
height: auto;
max-height: 400px;
object-fit: contain;
}
.hero-content::before {
display: none;
/* Remove the gradient since content and image are now stacked */
}
.hero-cta {
justify-content: center;
}
.tech-stack {
justify-content: center;
}
}
@media (max-width: 640px) {
.hero {
padding: 2rem 1rem;
}
.hero-content {
padding: 2rem 1rem;
}
.gradient-text {
font-size: 2.5rem;
}
.hero-cta {
flex-direction: column;
}
}
.logo {
width: 120px;
height: 120px;
margin-bottom: 1rem;
transition: transform 0.3s;
}
.logo:hover {
transform: scale(1.05);
}
.badges {
display: flex;
gap: 0.5rem;
justify-content: center;
/* Centered only on tablets and smaller screens */
@media (min-width: 1024px) {
justify-content: flex-start;
/* Align to the start on larger screens */
}
margin-top: 1rem;
}
.badge {
background: var(--primary);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
}
/* Features */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.feature {
background: var(--card);
padding: 2rem;
border-radius: 12px;
transition: transform 0.2s;
border: 1px solid var(--border);
}
.feature:hover {
transform: translateY(-4px);
}
/* Download Section */
.download-options {
display: flex;
gap: 1.5rem;
justify-content: center;
margin: 2rem 0;
flex-wrap: wrap;
padding: 0 1rem;
}
.download-button {
background: var(--primary-color);
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
text-decoration: none;
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.2s;
min-width: 250px;
}
.download-button .button-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.download-button .primary {
font-weight: 600;
font-size: 1rem;
}
.download-button .secondary {
font-size: 0.85rem;
opacity: 0.9;
}
/* New styles for nested links */
.download-button .secondary a {
color: inherit;
text-decoration: none;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
display: inline-block;
}
.download-button .secondary a:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.download-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.download-button .icon {
font-size: 1.5rem;
}
.button-text {
display: flex;
flex-direction: column;
}
.button-text .primary {
font-weight: 600;
}
.button-text .secondary {
font-size: 0.875rem;
opacity: 0.8;
}
/* Primary button style (can be used for other buttons too) */
.primary-button {
background: var(--primary-color);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.2s;
}
.primary-button:hover {
background: var(--secondary-color);
transform: translateY(-2px);
}
/* Secondary button style */
.secondary-button {
background: rgba(99, 102, 241, 0.1);
color: var(--primary-color);
padding: 0.75rem 1.5rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.2s;
}
.secondary-button:hover {
background: rgba(99, 102, 241, 0.2);
transform: translateY(-2px);
}
@media (max-width: 640px) {
.download-options {
flex-direction: column;
align-items: stretch;
}
.download-button {
justify-content: center;
}
}
/* Installation Steps */
.install-steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.step {
background: var(--card-background);
padding: 2rem;
border-radius: 12px;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: transform 0.2s ease;
}
.step:hover {
transform: translateY(-4px);
}
.step-number {
position: absolute;
top: -1rem;
left: -1rem;
background: var(--primary-color);
color: white;
width: 2rem;
height: 2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.step h3 {
margin-bottom: 1rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.step p {
color: var(--text-secondary);
line-height: 1.5;
}
/* Dark mode adjustments */
[data-theme="dark"] .step {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(30, 41, 59, 0.8);
}
@media (max-width: 768px) {
.install-steps {
grid-template-columns: 1fr;
}
}
/* Keyboard Shortcuts */
.shortcuts {
background: var(--card);
border-radius: 12px;
padding: 2rem;
margin-top: 2rem;
border: 1px solid var(--border);
}
.shortcut-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.shortcut {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
border-radius: 6px;
}
.shortcut:hover {
background: var(--hover);
}
.key {
background: var(--code-bg);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.875rem;
}
/* FAQ Section */
.faq {
max-width: 800px;
margin: 4rem auto;
padding: 0 2rem;
}
.faq-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.faq-item {
background: var(--card-background);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
}
.faq-question {
width: 100%;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
background: none;
border: none;
cursor: pointer;
text-align: left;
color: var(--text-primary);
font-size: 1.1rem;
font-weight: 500;
}
.faq-icon {
font-size: 1.5rem;
transition: transform 0.3s ease;
}
.faq-item.active .faq-icon {
transform: rotate(180deg);
}
.faq-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
padding: 0 1.5rem;
}
.faq-item.active .faq-answer {
max-height: 500px;
/* Adjust based on content */
padding: 0 1.5rem 1.5rem;
}
.faq-answer p {
color: var(--text-secondary);
line-height: 1.6;
}
.faq-answer ul {
margin-top: 0.5rem;
margin-left: 1.5rem;
color: var(--text-secondary);
}
.faq-answer li {
margin-bottom: 0.5rem;
}
/* Dark mode adjustments */
[data-theme="dark"] .faq-item {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(30, 41, 59, 0.8);
}
@media (max-width: 768px) {
.faq {
margin: 3rem auto;
}
.faq-question {
padding: 1.25rem;
font-size: 1rem;
}
}
/* Footer */
footer {
border-top: 1px solid var(--border);
margin-top: 6rem;
padding-top: 3rem;
}
.footer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.footer-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.footer-section a {
color: var(--text);
text-decoration: none;
opacity: 0.8;
}
.footer-section a:hover {
opacity: 1;
}
/* Responsive Design */
@media (max-width: 768px) {
nav {
flex-wrap: wrap;
gap: 0.5rem;
}
.download-options {
flex-direction: column;
}
.feature-grid {
grid-template-columns: 1fr;
}
.install-steps {
grid-template-columns: 1fr;
}
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.feature,
.step,
.download-button {
animation: fadeIn 0.5s ease-out;
}
.screenshot {
width: 100%;
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.screenshot-img {
width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.glass-nav {
backdrop-filter: blur(12px);
background: var(--card-background);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 1rem 2rem;
box-sizing: border-box;
box-shadow: none;
}
[data-theme="dark"] .glass-nav {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(30, 41, 59, 0.8);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.tech-badge {
background: rgba(99, 102, 241, 0.1);
color: var(--primary-color);
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 500;
margin-right: 0.5rem;
}
.feature-card {
background: var(--card-background);
border-radius: 16px;
padding: 2rem;
transition: transform 0.2s;
}
.feature-card:hover {
transform: translateY(-5px);
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
100% {
transform: translateY(0px);
}
}
/* Features Section */
.features {
padding: 4rem 2rem;
}
.section-title {
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
font-weight: 700;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.feature-card {
background: var(--card-background);
padding: 2rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: transform 0.2s ease;
}
.feature-card:hover {
transform: translateY(-4px);
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.feature-card p {
color: var(--text-secondary);
line-height: 1.5;
}
/* Dark mode adjustments */
[data-theme="dark"] .feature-card {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(30, 41, 59, 0.8);
}
@media (max-width: 768px) {
.feature-grid {
grid-template-columns: 1fr;
}
.section-title {
font-size: 2rem;
}
}
/* Footer Styles */
.footer {
background: var(--card-background);
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding: 4rem 2rem 2rem;
margin-top: 4rem;
width: 100%;
box-sizing: border-box;
box-shadow: none;
}
[data-theme="dark"] .footer {
background: rgba(30, 41, 59, 0.8);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 3rem;
margin-bottom: 3rem;
}
.footer-section h3 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-primary);
}
.footer-section h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-primary);
}
.footer-description {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 1rem;
}
.footer-links,
.tech-stack-list {
list-style: none;
padding: 0;
}
.footer-links li,
.tech-stack-list li {
margin-bottom: 0.75rem;
}
.footer-links a,
.tech-stack-list a {
color: var(--text-secondary);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
transition: color 0.2s ease;
}
.footer-links a:hover,
.tech-stack-list a:hover {
color: var(--primary-color);
}
.footer-bottom {
max-width: 1200px;
margin: 0 auto;
padding-top: 2rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
text-align: center;
color: var(--text-secondary);
}
[data-theme="dark"] .footer-bottom {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-bottom p {
margin-bottom: 0.5rem;
}
.footer-bottom a {
color: var(--primary-color);
text-decoration: none;
}
.footer-bottom a:hover {
text-decoration: underline;
}
.heart {
color: #ff4b4b;
display: inline-block;
animation: heartbeat 1.5s ease infinite;
}
.coffee {
display: inline-block;
animation: wiggle 1s ease infinite;
}
@keyframes heartbeat {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes wiggle {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-10deg);
}
75% {
transform: rotate(10deg);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.footer-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.footer {
padding: 3rem 1.5rem 1.5rem;
}
}
/* Dark mode adjustments */
[data-theme="dark"] .footer {
background: rgba(30, 41, 59, 0.8);
border-color: rgba(255, 255, 255, 0.1);
}
/* Common card styles */
.feature-card,
.step,
.faq-item {
background: var(--card-background);
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
box-shadow: none;
}
/* Dark mode specific adjustments */
[data-theme="dark"] .feature-card,
[data-theme="dark"] .step,
[data-theme="dark"] .faq-item {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(30, 41, 59, 0.8);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
}
/* Hover states */
.feature-card:hover,
.step:hover {
transform: translateY(-4px);
border-color: var(--primary-color);
}
.faq-item.active {
border-color: var(--primary-color);
}
/* Optional: Add subtle hover effect to nav links */
.nav-links a {
position: relative;
padding-bottom: 2px;
}
.nav-links a::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: var(--primary-color);
transition: width 0.3s ease;
}
.nav-links a:hover::after {
width: 100%;
}
/* Add this to your CSS */
.ph-badge {
display: inline-block;
transition: transform 0.2s ease;
}
.ph-badge:hover {
transform: translateY(-2px);
}
/* If you want to adjust for dark mode */
[data-theme="dark"] .ph-badge img {
filter: invert(1);
/* Optional: if you want to invert colors in dark mode */
}
.coming-soon-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #ff6154;
/* Product Hunt color */
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: transform 0.2s ease;
}
.coming-soon-badge:hover {
transform: translateY(-2px);
}
.trust-badge {
margin-top: 1.5rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.verified-icon {
color: #34C759;
font-weight: bold;
}
.verified-text {
color: var(--text-color);
}
.learn-more {
color: var(--primary-color);
text-decoration: none;
margin-left: 0.5rem;
}
.learn-more:hover {
text-decoration: underline;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.hero {
height: 500px;
}
.hero-content,
.hero-background {
flex: 0 0 100%;
}
}
.download-button.windows {
background: #00a4ef;
}
.download-button.windows:hover {
background: #0090d5;
}
.download-button.linux {
background: #E95420;
}
.download-button.linux:hover {
background: #C7431B;
}
.download-options-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.download-option {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
}
.download-option .format {
color: rgba(255, 255, 255, 0.8);
min-width: 65px;
}
.download-option .separator {
color: rgba(255, 255, 255, 0.4);
font-size: 0.7rem;
}
.download-option a {
color: inherit;
text-decoration: none;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
}
.download-option a:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.download-button.linux .secondary {
line-height: 1.6;
font-size: 0.85rem;
}
.download-button.linux .secondary a {
color: inherit;
text-decoration: none;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
}
.download-button.linux .secondary a:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.download-button.linux .secondary .separator {
opacity: 0.7;
margin: 0 2px;
}
.downloads {
padding: 2rem 0;
}
.download-container {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.download-container h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.download-subtitle {
color: var(--text-secondary);
margin-bottom: 3rem;
}
.download-options {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.download-group {
display: flex;
flex-direction: column;
gap: 1rem;
}
.platform-title {
font-size: 1.2rem;
color: var(--text-secondary);
text-align: left;
margin-bottom: 0.5rem;
}
.platform-options {
display: flex;
gap: 1rem;
justify-content: center;
}
.download-button {
background: var(--primary-color);
color: white;
padding: 1rem 1.5rem;
border-radius: 12px;
text-decoration: none;
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.2s;
border: 1px solid transparent;
min-width: 250px;
}
.download-button:hover {
transform: translateY(-2px);
background: var(--secondary-color);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
.download-button .icon {
font-size: 1.5rem;
}
.button-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
text-align: left;
}
.button-text .primary {
font-weight: 600;
font-size: 1rem;
}
.button-text .secondary {
font-size: 0.85rem;
opacity: 0.9;
}
.linux-options {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.format-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.format-label {
font-size: 0.9rem;
color: var(--text-secondary);
text-align: left;
}
.format-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.format-buttons .download-button {
min-width: 150px;
}
/* Dark mode adjustments */
[data-theme="dark"] .download-button {
background: rgba(30, 41, 59, 0.8);
border-color: rgba(255, 255, 255, 0.1);
}
[data-theme="dark"] .download-button:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.version-info {
text-align: center;
margin: 1rem 0;
font-size: 0.9rem;
color: var(--text-secondary);
}
#current-version {
font-weight: 600;
color: var(--primary-color);
}
/* Download Section Responsive Styles */
@media (max-width: 768px) {
.download-options {
flex-direction: column;
gap: 2rem;
}
.download-group {
width: 100%;
}
.platform-options {
flex-direction: column;
}
.download-button {
width: 100%;
margin: 0.5rem 0;
}
.linux-downloads {
width: 100%;
margin-top: 1rem;
}
.linux-downloads h4 {
margin-bottom: 0.5rem;
}
/* Adjust text size for better readability on mobile */
.button-text .primary {
font-size: 0.9rem;
}
.button-text .secondary {
font-size: 0.8rem;
}
}
/* Extra small devices */
@media (max-width: 480px) {
.download-container {
padding: 1rem;
}
.download-stats {
font-size: 0.9rem;
}
.platform-title {
font-size: 1.2rem;
}
}
.download-stats {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(var(--primary-color-rgb), 0.1);
border-radius: 6px;
margin: 1rem 0;
font-size: 0.9rem;
}
#download-count {
font-weight: bold;
color: var(--primary-color);
font-size: 1.1rem;
}
/* Adjust the existing download-subtitle margin */
.download-subtitle {
color: var(--text-secondary);
margin-bottom: 2rem;
margin-top: 0.5rem;
}
/* Testimonials Section */
.testimonials {
padding: 4rem 0;
background: var(--background);
max-width: 1200px;
margin: 0 auto;
}
.testimonial-grid {
column-count: 3;
column-gap: 2rem;
margin-top: 3rem;
padding: 0 2rem;
}
/* Ensure Twitter embeds stack properly */
.twitter-tweet {
margin: 0 0 2rem 0 !important;
width: 100% !important;
break-inside: avoid;
display: inline-block;
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.testimonial-grid {
column-count: 2;
}
}
@media (max-width: 768px) {
.testimonial-grid {
column-count: 1;
}
}
/* Mobile Navigation Styles */
@media (max-width: 768px) {
.glass-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
}
.menu-button {
display: block;
background: none;
border: none;
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
z-index: 101;
}
.nav-links {
position: fixed;
top: calc(60px + 1rem);
left: 0;
right: 0;
background: var(--card-background);
padding: 1.5rem;
flex-direction: column;
align-items: center;
gap: 1rem;
transform: translateY(-150%);
transition: transform 0.3s ease;
backdrop-filter: blur(12px);
visibility: hidden;
opacity: 0;
z-index: 100;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.nav-links.active {
transform: translateY(0);
visibility: visible;
opacity: 1;
}
}
/* Hide menu button on larger screens */
@media (min-width: 769px) {
.menu-button {
display: none;
}
}
================================================
FILE: jsconfig.json
================================================
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
================================================
FILE: package.json
================================================
{
"name": "neohtop",
"version": "1.2.0",
"description": "",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"tauri": "tauri",
"format": "prettier --write ./src && cargo fmt --manifest-path src-tauri/Cargo.toml",
"format:check": "prettier --check ./src && cargo fmt --manifest-path src-tauri/Cargo.toml -- --check",
"prepare": "husky install"
},
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@tauri-apps/api": "^2.0.3",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.2.1",
"simple-icons": "^13.15.0",
"svelte-fa": "^4.0.3"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.7.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tauri-apps/cli": "^2.0.4",
"husky": "^8.0.0",
"lint-staged": "^15.2.10",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.5.0",
"vite": "^6.3.5"
},
"lint-staged": {
"src/**/*.{js,ts,jsx,tsx,svelte}": [
"prettier --write"
],
"src-tauri/**/*.rs": [
"cargo fmt --manifest-path src-tauri/Cargo.toml --"
]
}
}
================================================
FILE: src/App.svelte
================================================
================================================
FILE: src/app.css
================================================
:root {
/* Default theme values will be overridden by theme store */
--base: #1e1e2e;
--mantle: #181825;
--crust: #11111b;
--text: #cdd6f4;
--subtext0: #a6adc8;
--subtext1: #bac2de;
--surface0: #313244;
--surface1: #45475a;
--surface2: #585b70;
--overlay0: #6c7086;
--overlay1: #7f849c;
--blue: #89b4fa;
--lavender: #b4befe;
--sapphire: #74c7ec;
--sky: #89dceb;
--red: #f38ba8;
--maroon: #eba0ac;
--peach: #fab387;
--yellow: #f9e2af;
--green: #a6e3a1;
--teal: #94e2d5;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
background-color: var(--base);
color: var(--text);
-webkit-font-smoothing: antialiased;
overflow: hidden;
user-select: none;
}
/* Global scrollbar styles */
* {
scrollbar-width: thin;
scrollbar-color: var(--surface2) var(--mantle);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--mantle);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--surface2);
border-radius: 4px;
transition: background 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: var(--surface1);
}
::-webkit-scrollbar-corner {
background: var(--mantle);
}
/* Glassy theme */
[data-theme="glassy"] body {
background: transparent !important;
}
[data-theme="glassy"] .toolbar {
position: relative;
background: rgba(24, 24, 37, 0.5) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
z-index: 9;
}
[data-theme="glassy"] .stat-panel {
background: rgba(24, 24, 37, 0.2) !important;
z-index: 100;
}
[data-theme="glassy"] .panel-header {
border-color: rgba(255, 255, 255, 0.1) !important;
}
[data-theme="glassy"] .col-actions {
background: rgba(24, 24, 37, 0.2) !important;
border-bottom: 1px solid rgba(232, 232, 232, 0.1) !important;
border-left: 1px solid rgba(232, 232, 232, 0.1) !important;
}
[data-theme="glassy"] .search-input,
[data-theme="glassy"] .btn-toggle,
[data-theme="glassy"] .btn-action,
[data-theme="glassy"] .info-button,
[data-theme="glassy"] .btn-page,
[data-theme="glassy"] .theme-button,
[data-theme="glassy"] .usage-pill,
[data-theme="glassy"] .bar-container,
[data-theme="glassy"] .select-input {
background: rgba(24, 24, 37, 0.2) !important;
z-index: 100;
}
[data-theme="glassy"] .btn-clear {
z-index: 100;
}
[data-theme="glassy"] tr.pinned,
[data-theme="glassy"] tr:hover {
background: rgba(24, 24, 37, 0.3) !important;
}
[data-theme="glassy"] th {
background: rgba(24, 24, 37, 0.5) !important;
}
[data-theme="glassy"] td {
border-bottom: 1px solid rgba(232, 232, 232, 0.1) !important;
background: rgba(24, 24, 37, 0.2) !important;
}
================================================
FILE: src/app.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<title>NeoHtop</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
================================================
FILE: src/lib/components/AppInfo.svelte
================================================
<script lang="ts">
import { getVersion } from "@tauri-apps/api/app";
import { onMount } from "svelte";
import { ThemeSwitcher } from "$lib/components";
import { faInfo } from "@fortawesome/free-solid-svg-icons";
import Fa from "svelte-fa";
import { ASCII_ART, APP_INFO } from "$lib/constants";
let version = "";
let latestVersion = "";
let showInfo = false;
let hasUpdate = false;
async function checkLatestVersion() {
try {
const response = await fetch(
"https://api.github.com/repos/abdenasser/neohtop/releases/latest",
);
const data = await response.json();
// Extract version number from tag (e.g., "1.0.6" from "macos-nightly-1.0.6")
const versionMatch = data.tag_name.match(/\d+\.\d+\.\d+/);
if (!versionMatch) {
console.warn(
"Unexpected version format in latest release:",
data.tag_name,
);
return;
}
latestVersion = versionMatch[0];
// Extract version number from current version
const currentVersionMatch = version.match(/\d+\.\d+\.\d+/);
if (!currentVersionMatch) {
console.warn("Unexpected current version format:", version);
return;
}
// Compare only the version numbers
hasUpdate = currentVersionMatch[0] !== latestVersion;
} catch (error) {
console.error("Failed to check latest version:", error);
latestVersion = "";
hasUpdate = false;
}
}
onMount(async () => {
try {
version = await getVersion();
await checkLatestVersion();
} catch (error) {
console.error("Failed to initialize version info:", error);
version = "";
}
});
</script>
<div class="app-info">
<ThemeSwitcher />
<button
class:info-button={true}
class:has-update={hasUpdate}
on:click={() => (showInfo = !showInfo)}
aria-label="Toggle app info"
>
<span class="icon" class:update-available={hasUpdate}>
<Fa icon={faInfo} />
</span>
</button>
{#if showInfo}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="info-panel" on:mouseleave={() => (showInfo = false)}>
<div class="info-content">
<pre class="ascii-art">{ASCII_ART}</pre>
<div class="details">
<div class="detail-row">
<span>NeoHtop v{version}</span>
{#if hasUpdate}
<a
href={`https://github.com/abdenasser/neohtop/releases/latest`}
class="update-button"
target="_blank"
rel="noopener noreferrer"
>
Update to v{latestVersion}
</a>
{/if}
</div>
<div class="detail-row">
<span class="label">app</span>
<span class="separator">::</span>
<span class="value">{APP_INFO.name}</span>
</div>
<div class="detail-row">
<span class="label">source</span>
<span class="separator">::</span>
<a
href={APP_INFO.github}
class="value"
target="_blank"
rel="noopener noreferrer"
>
{APP_INFO.github}
</a>
</div>
<div class="detail-row">
<span class="label">stack</span>
<span class="separator">::</span>
<span class="value">{APP_INFO.stack.join(", ")}</span>
</div>
</div>
</div>
</div>
{/if}
</div>
<style>
.app-info {
display: flex;
gap: 8px;
position: relative;
}
.info-button,
:global(.theme-button) {
height: 28px;
padding: 0 12px;
font-size: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
}
.info-button:hover,
:global(.theme-button:hover) {
background: var(--surface1);
}
.icon {
display: inline-flex;
align-items: center;
font-size: 10px;
color: var(--subtext0);
}
.icon.update-available {
color: var(--red);
}
.info-panel {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
padding: 16px;
background: var(--base);
border: 1px solid var(--surface0);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
z-index: 100;
min-width: 400px;
}
.info-content {
display: flex;
gap: 24px;
}
.ascii-art {
font-family: monospace;
font-size: 8px;
line-height: 1;
color: var(--mauve);
margin: 0;
padding: 0;
white-space: pre;
}
.details {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.detail-row {
display: flex;
align-items: center;
gap: 8px;
font-family: monospace;
font-size: 13px;
}
.label {
color: var(--green);
min-width: 80px;
}
.separator {
color: var(--subtext0);
}
.value {
color: var(--text);
}
.detail-row span {
color: var(--text);
font-weight: 500;
}
.info-button.has-update {
border-color: var(--red);
}
.info-button.has-update:hover {
background: color-mix(in srgb, var(--red) 10%, transparent);
}
.info-button.has-update .icon {
color: var(--red);
}
.update-button {
display: inline-flex;
align-items: center;
padding: 4px 8px;
font-size: 12px;
color: var(--base);
background: var(--red);
border-radius: 4px;
text-decoration: none;
margin-left: 8px;
transition: all 0.2s ease;
}
.update-button:hover {
background: color-mix(in srgb, var(--red) 90%, white);
transform: translateY(-1px);
}
</style>
================================================
FILE: src/lib/components/ThemeSwitcher.svelte
================================================
<script lang="ts">
import { themeStore } from "$lib/stores";
import { overlayStore } from "$lib/stores/overlay";
import { themes } from "$lib/definitions";
import Fa from "svelte-fa";
import {
faChevronDown,
faChevronRight,
faChevronLeft,
} from "@fortawesome/free-solid-svg-icons";
import { platform } from "@tauri-apps/plugin-os";
import { THEME_GROUPS } from "$lib/constants";
import { onDestroy } from "svelte";
let containerElement: HTMLDivElement;
let overlayElement: HTMLDivElement;
let optionsContainer: HTMLDivElement;
let canScrollLeft = false;
let canScrollRight = false;
$: showMenu = $overlayStore === "theme";
const themeGroups = [
...THEME_GROUPS,
...(platform() === "windows" || platform() === "macos"
? [
{
label: "Glassy",
themes: ["glassy"],
},
]
: []),
];
function updateOverlayPosition() {
if (overlayElement && containerElement) {
const toolbarContent = containerElement.closest(".toolbar-content");
if (toolbarContent) {
const toolbarRect = toolbarContent.getBoundingClientRect();
const containerRect = containerElement.getBoundingClientRect();
const leftOffset = containerRect.left - toolbarRect.left;
const rightOffset = toolbarRect.right - containerRect.right;
const topOffset = containerRect.top - toolbarRect.top;
overlayElement.style.left = `-${leftOffset}px`;
overlayElement.style.right = `-${rightOffset}px`;
overlayElement.style.top = `-${topOffset}px`;
}
}
}
function updateScrollButtons() {
if (optionsContainer) {
canScrollLeft = optionsContainer.scrollLeft > 0;
canScrollRight =
optionsContainer.scrollLeft <
optionsContainer.scrollWidth - optionsContainer.clientWidth;
}
}
function scrollLeft() {
if (optionsContainer) {
optionsContainer.scrollBy({ left: -200, behavior: "smooth" });
setTimeout(updateScrollButtons, 100);
}
}
function scrollRight() {
if (optionsContainer) {
optionsContainer.scrollBy({ left: 200, behavior: "smooth" });
setTimeout(updateScrollButtons, 100);
}
}
function toggleThemeMenu(event: Event) {
event.stopPropagation();
if (showMenu) {
overlayStore.close();
} else {
overlayStore.open("theme");
setTimeout(() => {
updateOverlayPosition();
updateScrollButtons();
}, 0);
}
}
function selectTheme(themeName: string) {
themeStore.setTheme(themeName);
overlayStore.close();
}
function handleClickOutside(event: MouseEvent) {
if (
showMenu &&
containerElement &&
!containerElement.contains(event.target as Node)
) {
overlayStore.close();
}
}
function setupClickOutside() {
if (typeof document !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
}
function cleanupClickOutside() {
if (typeof document !== "undefined") {
document.removeEventListener("click", handleClickOutside);
}
}
$: if (showMenu) {
setTimeout(setupClickOutside, 0);
} else {
cleanupClickOutside();
}
onDestroy(() => {
cleanupClickOutside();
});
</script>
<div
class="theme-switcher"
class:active={showMenu}
bind:this={containerElement}
>
<button
class="theme-button"
class:active={showMenu}
on:click={toggleThemeMenu}
aria-label="Toggle theme menu"
>
<div class="current-theme">
<div class="theme-preview" style:background={$themeStore.colors.base}>
<div
class="preview-color"
style:background={$themeStore.colors.blue}
></div>
<div
class="preview-color"
style:background={$themeStore.colors.red}
></div>
<div
class="preview-color"
style:background={$themeStore.colors.green}
></div>
</div>
</div>
<span class="icon">
{#if showMenu}
<Fa icon={faChevronDown} />
{:else}
<Fa icon={faChevronRight} />
{/if}
</span>
</button>
{#if showMenu}
<div
class="touchbar-full-overlay"
bind:this={overlayElement}
on:click={() => overlayStore.close()}
on:keydown={(e) => e.key === "Escape" && overlayStore.close()}
role="dialog"
aria-label="Theme selection"
tabindex="-1"
>
{#if canScrollLeft}
<button
class="scroll-chevron scroll-left"
on:click|stopPropagation={scrollLeft}
>
<Fa icon={faChevronLeft} />
</button>
{/if}
<div
class="touchbar-horizontal-options"
bind:this={optionsContainer}
on:scroll={updateScrollButtons}
>
{#each themeGroups as group}
{#each group.themes as themeName}
{@const theme = themes[themeName]}
<button
class="touchbar-option"
class:active={$themeStore.name === theme.name}
on:click|stopPropagation={() => selectTheme(theme.name)}
title={theme.label}
>
<div class="theme-preview" style:background={theme.colors.base}>
<div
class="preview-color"
style:background={theme.colors.blue}
></div>
<div
class="preview-color"
style:background={theme.colors.red}
></div>
<div
class="preview-color"
style:background={theme.colors.green}
></div>
</div>
<span class="theme-label">{theme.label}</span>
</button>
{/each}
{/each}
</div>
{#if canScrollRight}
<button
class="scroll-chevron scroll-right"
on:click|stopPropagation={scrollRight}
>
<Fa icon={faChevronRight} />
</button>
{/if}
</div>
{/if}
</div>
<style>
.theme-switcher {
position: relative;
}
.theme-button {
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
}
.theme-button:hover {
background: var(--surface1);
border-color: var(--blue);
}
.theme-button.active {
background: var(--surface1);
border-color: var(--blue);
}
.icon {
font-size: 10px;
color: var(--subtext0);
}
.touchbar-full-overlay {
position: absolute;
top: -0px;
height: 44px;
background: var(--mantle);
border: none;
border-radius: 0;
box-shadow: none;
z-index: 1000;
display: flex;
align-items: center;
padding: 0 8px;
gap: 8px;
}
.scroll-chevron {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
flex-shrink: 0;
font-size: 10px;
animation: optionSlideIn 0.2s ease-out;
animation-fill-mode: both;
animation-delay: 0ms;
}
.scroll-chevron:hover {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-horizontal-options {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 0 4px;
}
.touchbar-horizontal-options::-webkit-scrollbar {
display: none;
}
.touchbar-option {
padding: 0 8px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: fit-content;
box-sizing: border-box;
animation: optionSlideIn 0.2s ease-out;
animation-fill-mode: both;
}
.touchbar-option:nth-child(1) {
animation-delay: 20ms;
}
.touchbar-option:nth-child(2) {
animation-delay: 40ms;
}
.touchbar-option:nth-child(3) {
animation-delay: 60ms;
}
.touchbar-option:nth-child(4) {
animation-delay: 80ms;
}
.touchbar-option:nth-child(5) {
animation-delay: 100ms;
}
.touchbar-option:nth-child(6) {
animation-delay: 120ms;
}
.touchbar-option:nth-child(7) {
animation-delay: 140ms;
}
.touchbar-option:nth-child(8) {
animation-delay: 160ms;
}
.touchbar-option:nth-child(9) {
animation-delay: 180ms;
}
.touchbar-option:nth-child(10) {
animation-delay: 200ms;
}
@keyframes optionSlideIn {
from {
opacity: 0;
transform: translateY(-8px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.touchbar-option:hover:not(.disabled) {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-option.active {
background: var(--blue);
color: var(--base);
border-color: var(--blue);
}
.theme-preview {
display: flex;
gap: 2px;
padding: 2px;
border-radius: 3px;
border: 1px solid var(--surface1);
}
.preview-color {
width: 8px;
height: 8px;
border-radius: 1px;
}
.theme-label {
font-size: 12px;
}
.current-theme {
display: flex;
align-items: center;
gap: 8px;
}
</style>
================================================
FILE: src/lib/components/TitleBar.svelte
================================================
<script lang="ts">
</script>
<div class="title-bar" data-tauri-drag-region>
<div class="title">
<img src="/32x32.png" alt="NeoHtop" class="app-icon" />
<div class="neon">NeoHtop</div>
</div>
</div>
<style>
.title-bar {
height: 32px;
/* background: var(--mantle); */
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
-webkit-user-select: none;
user-select: none;
position: relative;
overflow: hidden;
}
.title {
display: flex;
align-items: center;
position: relative;
height: 100%;
}
.neon {
font-family: "Courier New", monospace;
font-size: 14px;
font-weight: bold;
color: var(--text);
}
.app-icon {
width: 24px;
height: 24px;
margin-right: 4px;
display: flex;
align-items: center;
}
</style>
================================================
FILE: src/lib/components/index.ts
================================================
export * from "./toolbar";
export * from "./process";
export * from "./stats";
export * from "./modals";
export { default as AppInfo } from "./AppInfo.svelte";
export { default as TitleBar } from "./TitleBar.svelte";
export { default as ThemeSwitcher } from "./ThemeSwitcher.svelte";
================================================
FILE: src/lib/components/modals/KillProcessModal.svelte
================================================
<script lang="ts">
import { Modal } from "$lib/components";
interface Process {
pid: number;
name: string;
}
export let show = false;
export let process: Process | null = null;
export let onClose: () => void;
export let onConfirm: () => Promise<void>;
export let isKilling = false;
</script>
<Modal {show} title="Confirm Action" maxWidth="400px" {onClose}>
{#if process}
<div class="confirm-content">
<p class="confirm-message">Are you sure you want to end this process?</p>
<div class="process-info">
<span class="process-name">{process.name}</span>
<span class="process-pid">(PID: {process.pid})</span>
</div>
<div class="confirm-actions">
<button class="btn-secondary" on:click={onClose} disabled={isKilling}>
Cancel
</button>
<button class="btn-danger" on:click={onConfirm} disabled={isKilling}>
{#if isKilling}
<div class="spinner"></div>
<span>Ending...</span>
{:else}
End Process
{/if}
</button>
</div>
</div>
{/if}
</Modal>
<style>
.confirm-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.confirm-message {
color: var(--text);
margin: 0;
font-size: 14px;
}
.process-info {
background: var(--mantle);
padding: 12px;
border-radius: 6px;
}
.process-name {
color: var(--text);
font-weight: 500;
font-size: 14px;
}
.process-pid {
color: var(--subtext0);
font-size: 12px;
margin-left: 8px;
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn-secondary {
padding: 8px 16px;
font-size: 13px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: var(--surface1);
}
.btn-danger {
padding: 8px 16px;
font-size: 13px;
color: var(--base);
background: var(--red);
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-danger:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-danger:hover {
background: color-mix(in srgb, var(--red) 90%, white);
}
.spinner {
width: 16px;
}
</style>
================================================
FILE: src/lib/components/modals/Modal.svelte
================================================
<script lang="ts">
export let show = false;
export let maxWidth = "600px";
export let title: string;
export let onClose: () => void;
</script>
{#if show}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-backdrop" on:click={onClose}>
<div class="modal" on:click|stopPropagation style="--max-width: {maxWidth}">
<div class="modal-header">
<h2>{title}</h2>
<button class="btn-close" on:click={onClose}>×</button>
</div>
<div class="modal-content">
<slot />
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--base);
border-radius: 8px;
width: 100%;
max-width: var(--max-width);
max-height: 90vh;
overflow: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--surface0);
}
.modal-header h2 {
margin: 0;
font-size: 18px;
color: var(--text);
}
.btn-close {
background: transparent;
border: none;
color: var(--overlay0);
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
}
.btn-close:hover {
color: var(--text);
}
.modal-content {
padding: 16px;
}
</style>
================================================
FILE: src/lib/components/modals/ProcessDetailsModal.svelte
================================================
<script lang="ts">
import { Modal } from "$lib/components";
import { formatBytes } from "$lib/utils";
import type { Process } from "$lib/types";
import Fa from "svelte-fa";
import {
faMemory,
faMicrochip,
faCodeFork,
faTerminal,
faList,
} from "@fortawesome/free-solid-svg-icons";
export let show = false;
export let process: Process | null = null;
export let onClose: () => void;
export let processes: Process[] = [];
export let onShowDetails: (process: Process) => void;
$: childProcesses = process
? processes.filter((p) => p.ppid === process.pid)
: [];
</script>
<Modal
{show}
title={`${process ? process.name.slice(0, 10) : "Unknown Process"} - Process Details`}
maxWidth="1000px"
{onClose}
>
{#if process}
<div class="modal-content">
<!-- Header Stats -->
<div class="header-stats">
<div class="stat-item">
<div class="stat-label">PID</div>
<div class="stat-value">{process.pid}</div>
</div>
<div class="stat-item">
<div class="stat-label">Status</div>
<div
class="stat-value status"
class:running={process.status === "Running"}
>
{process.status}
</div>
</div>
<div class="stat-item">
<div class="stat-label">CPU</div>
<div class="stat-value">{process.cpu_usage.toFixed(1)}%</div>
</div>
<div class="stat-item">
<div class="stat-label">Memory</div>
<div class="stat-value">{formatBytes(process.memory_usage)}</div>
</div>
</div>
<!-- Main Content -->
<div class="content-grid">
<!-- Left Column -->
<div class="content-column">
<!-- Process Info -->
<div class="card">
<div class="card-header">
<Fa icon={faMicrochip} />
<span>Process Information</span>
</div>
<div class="card-content">
<div class="info-grid">
<div class="info-item">
<span class="info-label">Name</span>
<span class="info-value">{process.name}</span>
</div>
<div class="info-item">
<span class="info-label">User</span>
<span class="info-value">{process.user}</span>
</div>
<div class="info-item">
<span class="info-label">Parent PID</span>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="info-value clickable"
on:click={() => {
const parent = processes.find(
(p) => p.pid === process.ppid,
);
if (parent) onShowDetails(parent);
}}
>
{process.ppid}
</span>
</div>
<div class="info-item">
<span class="info-label">Session ID</span>
<span class="info-value">{process.session_id}</span>
</div>
</div>
</div>
</div>
<!-- Resource Usage -->
<div class="card">
<div class="card-header">
<Fa icon={faMemory} />
<span>Resource Usage</span>
</div>
<div class="card-content">
<div class="resource-grid">
<div class="resource-item">
<div class="resource-header">
<span>CPU Usage</span>
<span class="resource-value"
>{process.cpu_usage.toFixed(1)}%</span
>
</div>
<div class="progress-bar">
<div
class="progress-fill"
style="width: {process.cpu_usage}%"
class:high={process.cpu_usage > 50}
class:critical={process.cpu_usage > 80}
></div>
</div>
</div>
<div class="resource-item">
<div class="resource-header">
<span>Memory Usage</span>
</div>
<div class="memory-stats">
<div>Physical: {formatBytes(process.memory_usage)}</div>
<div>Virtual: {formatBytes(process.virtual_memory)}</div>
</div>
</div>
<div class="resource-item">
<div class="resource-header">
<span>Disk I/O</span>
</div>
<div class="disk-stats">
<div>Read: {formatBytes(process.disk_usage[0])}</div>
<div>Written: {formatBytes(process.disk_usage[1])}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column -->
<div class="content-column">
<!-- Command -->
<div class="card">
<div class="card-header">
<Fa icon={faTerminal} />
<span>Command</span>
</div>
<div class="card-content">
<div class="command-text">{process.command}</div>
<div class="path-text">{process.root}</div>
</div>
</div>
<!-- Child Processes -->
{#if childProcesses.length > 0}
<div class="card">
<div class="card-header">
<Fa icon={faCodeFork} />
<span>Child Processes ({childProcesses.length})</span>
</div>
<div class="card-content">
<table class="process-table">
<thead>
<tr>
<th>Name</th>
<th>PID</th>
<th>CPU</th>
<th>Memory</th>
</tr>
</thead>
<tbody>
{#each childProcesses as child}
<tr
class="clickable"
on:click={() => onShowDetails(child)}
>
<td>{child.name}</td>
<td>{child.pid}</td>
<td>{child.cpu_usage.toFixed(1)}%</td>
<td>{formatBytes(child.memory_usage)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
<!-- Environment Variables -->
{#if process.environ.length > 0}
<div class="card">
<div class="card-header">
<Fa icon={faList} />
<span>Environment Variables</span>
</div>
<div class="card-content">
<div class="env-list">
{#each process.environ as env}
<div class="env-item">{env}</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
{/if}
</Modal>
<style>
/* Base Modal Content */
.modal-content {
display: flex;
flex-direction: column;
gap: 24px;
font-size: 13px;
color: var(--text);
}
/* Header Stats */
.header-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
padding: 16px;
background: var(--surface0);
border-radius: 8px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: var(--subtext0);
font-weight: 500;
}
.stat-value {
font-size: 16px;
font-weight: 600;
}
.stat-value.status {
color: var(--subtext0);
}
.stat-value.status.running {
color: var(--green);
}
/* Main Content Grid */
.content-grid {
display: grid;
grid-template-columns: minmax(300px, 0.4fr) minmax(400px, 0.6fr);
gap: 24px;
}
.content-column {
display: flex;
flex-direction: column;
gap: 24px;
min-width: 0; /* Prevent overflow issues */
}
/* Cards */
.card {
background: var(--surface0);
border-radius: 8px;
overflow: hidden;
min-width: 0; /* Prevent overflow issues */
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--surface1);
color: var(--subtext0);
font-weight: 500;
}
.card-header :global(svg) {
width: 14px;
height: 14px;
color: var(--blue);
}
.card-content {
padding: 16px;
overflow: auto;
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
color: var(--subtext0);
font-size: 12px;
}
.info-value {
color: var(--text);
}
.info-value.clickable {
cursor: pointer;
color: var(--blue);
}
.info-value.clickable:hover {
text-decoration: underline;
}
/* Resource Usage */
.resource-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.resource-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-header {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--subtext0);
font-size: 12px;
}
.resource-value {
color: var(--text);
}
/* Progress Bar */
.progress-bar {
height: 6px;
background: var(--surface1);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--blue);
transition: width 0.2s ease;
}
.progress-fill.high {
background: var(--yellow);
}
.progress-fill.critical {
background: var(--red);
}
/* Memory and Disk Stats */
.memory-stats,
.disk-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
color: var(--text);
}
/* Command and Path */
.command-text,
.path-text {
word-break: break-all;
white-space: pre-wrap;
}
.path-text {
margin-top: 8px;
font-size: 12px;
color: var(--subtext0);
}
/* Process Table */
.process-table {
width: 100%;
border-collapse: collapse;
}
.process-table th {
text-align: left;
padding: 8px;
color: var(--subtext0);
font-weight: 500;
border-bottom: 1px solid var(--surface1);
}
.process-table td {
padding: 8px;
border-bottom: 1px solid var(--surface1);
}
.process-table tr:last-child td {
border-bottom: none;
}
.process-table tr.clickable {
cursor: pointer;
}
.process-table tr.clickable:hover {
background: var(--surface1);
}
/* Environment Variables */
.env-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 200px;
overflow-y: auto;
margin: -16px;
padding: 16px;
}
.env-item {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
padding: 4px 8px;
border-radius: 4px;
color: var(--subtext1);
font-size: 12px;
}
.env-item:hover {
background: var(--surface1);
}
/* Update scrollbar styles to match the container edges */
.env-list::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.env-list::-webkit-scrollbar-track {
background: var(--surface0);
border-radius: 0;
}
.env-list::-webkit-scrollbar-thumb {
background: var(--surface2);
border-radius: 4px;
border: 2px solid var(--surface0);
}
.env-list::-webkit-scrollbar-thumb:hover {
background: var(--surface1);
}
/* Scrollbar Styles */
:global(.modal-content *::-webkit-scrollbar) {
width: 8px;
height: 8px;
}
:global(.modal-content *::-webkit-scrollbar-track) {
background: var(--mantle);
border-radius: 4px;
}
:global(.modal-content *::-webkit-scrollbar-thumb) {
background: var(--surface2);
border-radius: 4px;
}
:global(.modal-content *::-webkit-scrollbar-thumb:hover) {
background: var(--surface1);
}
/* Responsive Design */
@media (max-width: 900px) {
.content-grid {
grid-template-columns: 1fr;
}
.header-stats {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
================================================
FILE: src/lib/components/modals/index.ts
================================================
export { default as Modal } from "./Modal.svelte";
export { default as ProcessDetailsModal } from "./ProcessDetailsModal.svelte";
export { default as KillProcessModal } from "./KillProcessModal.svelte";
================================================
FILE: src/lib/components/process/ActionButtons.svelte
================================================
<script lang="ts">
import {
faThumbtack,
faInfoCircle,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import Fa from "svelte-fa";
import type { Process } from "$lib/types";
export let process: Process;
export let isPinned: boolean;
export let onTogglePin: (command: string) => void;
export let onShowDetails: (process: Process) => void;
export let onKillProcess: (process: Process) => void;
</script>
<td class="col-actions">
<div class="action-buttons">
<button
class="btn-action pin-btn"
class:pinned={isPinned}
on:click={() => onTogglePin(process.command)}
title={isPinned ? "Unpin" : "Pin"}
>
<Fa icon={faThumbtack} />
</button>
<button
class="btn-action info-btn"
on:click={() => onShowDetails(process)}
title="Show Details"
>
<Fa icon={faInfoCircle} />
</button>
<button
class="btn-action kill-btn"
on:click={() => onKillProcess(process)}
title="End Process"
>
<Fa icon={faXmark} />
</button>
</div>
</td>
<style>
td {
padding: 6px 12px;
border-bottom: 1px solid var(--surface0);
color: var(--text);
z-index: 1;
}
.col-actions {
position: sticky;
right: 0;
z-index: 2;
background: var(--base);
border-left: 1px solid var(--surface0);
width: 120px;
}
.action-buttons {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
}
.btn-action {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
background: transparent;
border-radius: 6px;
width: 28px;
height: 28px;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.btn-action::before {
content: "";
position: absolute;
inset: 0;
opacity: 0.1;
transition: opacity 0.2s ease;
}
.btn-action:hover::before {
opacity: 0.15;
}
.pin-btn {
color: var(--sapphire);
}
.pin-btn::before {
background: var(--sapphire);
}
.pin-btn.pinned {
color: var(--blue);
transform: rotate(45deg);
}
.pin-btn.pinned::before {
background: var(--blue);
opacity: 0.15;
}
.info-btn {
color: var(--lavender);
}
.info-btn::before {
background: var(--lavender);
}
.kill-btn {
color: var(--red);
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
}
.kill-btn:hover {
color: var(--base);
background: var(--red);
}
.kill-btn:hover::before {
opacity: 1;
}
.btn-action:hover {
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
}
.btn-action:active {
transform: translateY(1px);
}
.pin-btn.pinned:active {
transform: rotate(45deg) translateY(1px);
}
.btn-action:focus {
outline: none;
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
}
.btn-action:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-action:disabled:hover {
transform: none;
box-shadow: none;
}
.btn-action:disabled::before {
display: none;
}
</style>
================================================
FILE: src/lib/components/process/ProcessIcon.svelte
================================================
<script lang="ts">
import * as SimpleIcons from "simple-icons";
export let processName: string;
export let size: number = 16;
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
img.src = getIconForProcess("default");
img.onerror = null;
}
function getIconForProcess(name: string): string {
// First try with com.company.something pattern
if (name.startsWith("com.")) {
const companyName = name.replace(/^com\.([^.]+)\..*$/, "$1");
const formattedCompanyName =
companyName.charAt(0).toUpperCase() + companyName.slice(1);
const companyIconKey = `si${formattedCompanyName}`;
const companyIcon =
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
if (companyIcon) {
// Use theme color instead of brand color
const color = getComputedStyle(document.documentElement)
.getPropertyValue("--text")
.trim();
const svg =
typeof companyIcon === "object" && "svg" in companyIcon
? companyIcon.svg
: "";
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
}
}
// If no company icon found, fall back to original implementation
const cleanName = name
.replace(/\.(app|exe)$/i, "")
.replace(/[-_./\\]/g, " ")
.split(" ")[0]
.trim()
.toLowerCase();
const formattedName =
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
const iconKey = `si${formattedName}`;
let simpleIcon = SimpleIcons[iconKey as keyof typeof SimpleIcons];
// Default icon if no match found
if (!simpleIcon) {
simpleIcon = SimpleIcons.siGhostery;
}
// Use theme color instead of brand color
const color = getComputedStyle(document.documentElement)
.getPropertyValue("--text")
.trim();
const svg =
typeof simpleIcon === "object" && "svg" in simpleIcon
? simpleIcon.svg
: "";
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
}
</script>
<img
class="process-icon"
src={getIconForProcess(processName)}
alt=""
height={size}
width={size}
on:error={handleImageError}
/>
<style>
.process-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
</style>
================================================
FILE: src/lib/components/process/ProcessRow.svelte
================================================
<script lang="ts">
import type { Process, Column } from "$lib/types";
import { ProcessIcon, ActionButtons } from "$lib/components";
export let process: Process;
export let columns: Column[];
export let isPinned: boolean;
export let isHighUsage: boolean;
export let onTogglePin: (command: string) => void;
export let onShowDetails: (process: Process) => void;
export let onKillProcess: (process: Process) => void;
</script>
<tr class:high-usage={isHighUsage} class:pinned={isPinned}>
{#each columns.filter((col) => col.visible) as column}
<td class="truncate">
{#if column.id === "name"}
<div class="name-cell">
<ProcessIcon processName={process.name} />
<span class="process-name">{process.name}</span>
</div>
{:else if column.format}
{@html column.format(process[column.id])}
{:else}
{process[column.id]}
{/if}
</td>
{/each}
<ActionButtons
{process}
{isPinned}
{onTogglePin}
{onShowDetails}
{onKillProcess}
/>
</tr>
<style>
td {
padding: 6px 12px;
border-bottom: 1px solid var(--surface0);
color: var(--text);
z-index: 1;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 0;
}
tr:hover {
background-color: var(--surface0);
}
.high-usage {
background-color: color-mix(in srgb, var(--red) 10%, transparent);
}
.high-usage:hover {
background-color: color-mix(in srgb, var(--red) 15%, transparent);
}
tr.pinned {
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
}
tr.pinned:hover {
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
}
.name-cell {
display: flex;
align-items: center;
gap: 8px;
}
</style>
================================================
FILE: src/lib/components/process/ProcessTable.svelte
================================================
<script lang="ts">
import type { Process, Column } from "$lib/types";
import { TableHeader, ProcessRow } from "$lib/components";
export let processes: Process[];
export let columns: Column[];
export let systemStats: { memory_total: number } | null;
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
export let pinnedProcesses: Set<string>;
export let onToggleSort: (field: keyof Process) => void;
export let onTogglePin: (command: string) => void;
export let onShowDetails: (process: Process) => void;
export let onKillProcess: (process: Process) => void;
</script>
<div class="table-container">
<table>
<TableHeader {columns} {sortConfig} {onToggleSort} />
<tbody>
{#each processes as process (process.pid)}
<ProcessRow
{process}
{columns}
isPinned={pinnedProcesses.has(process.command)}
isHighUsage={process.cpu_usage > 50 ||
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
{onTogglePin}
{onShowDetails}
{onKillProcess}
/>
{/each}
</tbody>
</table>
</div>
<style>
.table-container {
flex: 1;
overflow-x: auto;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--surface2) var(--mantle);
}
.table-container::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.table-container::-webkit-scrollbar-track {
background: var(--mantle);
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb {
background: var(--surface2);
border-radius: 4px;
transition: background 0.2s ease;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: var(--surface1);
}
.table-container::-webkit-scrollbar-corner {
background: var(--mantle);
}
table {
width: max-content;
min-width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 13px;
}
</style>
================================================
FILE: src/lib/components/process/TableHeader.svelte
================================================
<script lang="ts">
import type { Process, Column } from "$lib/types";
export let columns: Column[];
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
export let onToggleSort: (field: keyof Process) => void;
function getSortIndicator(field: keyof Process) {
if (sortConfig.field !== field) return "↕";
return sortConfig.direction === "asc" ? "↑" : "↓";
}
</script>
<thead>
<tr>
{#each columns.filter((col) => col.visible) as column}
<th class="sortable" on:click={() => onToggleSort(column.id)}>
<div class="th-content">
{column.label}
<span
class="sort-indicator"
class:active={sortConfig.field === column.id}
>
{getSortIndicator(column.id)}
</span>
</div>
</th>
{/each}
<th>Actions</th>
</tr>
</thead>
<style>
th {
position: sticky;
top: 0;
background: var(--mantle);
text-align: left;
padding: 8px 12px;
font-weight: 500;
color: var(--subtext0);
border-bottom: 1px solid var(--surface0);
transition: background-color 0.2s ease;
z-index: 3;
}
th:last-child {
width: 120px;
min-width: 120px;
max-width: 120px;
}
.sortable {
cursor: pointer;
user-select: none;
}
.th-content {
display: flex;
align-items: center;
gap: 8px;
}
.sort-indicator {
color: var(--overlay0);
font-size: 12px;
opacity: 0.5;
transition: all 0.2s ease;
}
.sort-indicator.active {
color: var(--blue);
opacity: 1;
}
.sortable:hover .sort-indicator {
opacity: 1;
}
</style>
================================================
FILE: src/lib/components/process/index.ts
================================================
export { default as ProcessTable } from "./ProcessTable.svelte";
export { default as ProcessRow } from "./ProcessRow.svelte";
export { default as TableHeader } from "./TableHeader.svelte";
export { default as ActionButtons } from "./ActionButtons.svelte";
export { default as ProcessIcon } from "./ProcessIcon.svelte";
================================================
FILE: src/lib/components/stats/CpuPanel.svelte
================================================
<script lang="ts">
import { faMicrochip } from "@fortawesome/free-solid-svg-icons";
import { PanelHeader, ProgressBar } from "$lib/components";
import { formatPercentage } from "$lib/utils";
export let cpuUsage: number[];
$: averageUsage = formatPercentage(
cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length,
);
</script>
<div class="stat-panel">
<PanelHeader icon={faMicrochip} title="CPU Usage" usageValue={averageUsage} />
<div class="stats-content cpu-grid">
{#each cpuUsage as usage, i}
<div class="stat-item with-progress">
<ProgressBar
label={`Core ${i}`}
value={usage}
labelWidth="2.5rem"
valueWidth="2.5rem"
/>
</div>
{/each}
</div>
</div>
<style>
.stat-panel {
flex: 2.5;
min-width: 0;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cpu-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem 2rem;
height: auto;
overflow: visible;
}
.stat-item.with-progress {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
</style>
================================================
FILE: src/lib/components/stats/MemoryPanel.svelte
================================================
<script lang="ts">
import { faMemory } from "@fortawesome/free-solid-svg-icons";
import { PanelHeader, ProgressBar, StatItem } from "$lib/components";
import { formatMemorySize, formatPercentage } from "$lib/utils";
export let memoryTotal: number;
export let memoryUsed: number;
export let memoryFree: number;
$: memoryPercentage = (memoryUsed / memoryTotal) * 100;
</script>
<div class="stat-panel">
<PanelHeader
icon={faMemory}
title="Memory"
usageValue={formatPercentage(memoryPercentage)}
/>
<div class="stats-content">
<div class="stat-item with-progress">
<ProgressBar
label="Memory usage"
value={memoryPercentage}
labelWidth="5rem"
valueWidth="2.5rem"
/>
</div>
<StatItem label="Total" value={formatMemorySize(memoryTotal)} />
<StatItem label="Used" value={formatMemorySize(memoryUsed)} />
<StatItem label="Free" value={formatMemorySize(memoryFree)} />
</div>
</div>
<style>
.stat-panel {
flex: 2;
min-width: 0;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stat-item.with-progress {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
</style>
================================================
FILE: src/lib/components/stats/NetworkPanel.svelte
================================================
<script lang="ts">
import { faNetworkWired } from "@fortawesome/free-solid-svg-icons";
import { PanelHeader, StatItem } from "$lib/components";
import { formatBytes } from "$lib/utils";
export let networkRxBytes: number;
export let networkTxBytes: number;
</script>
<div class="stat-panel">
<PanelHeader icon={faNetworkWired} title="Network I/O" />
<div class="network-stats">
<StatItem label="↓ Receiving" value={formatBytes(networkRxBytes)} />
<StatItem label="↑ Sending" value={formatBytes(networkTxBytes)} />
</div>
</div>
<style>
.stat-panel {
flex: 0.8;
min-width: 125px;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.network-stats {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
</style>
================================================
FILE: src/lib/components/stats/PanelHeader.svelte
================================================
<script lang="ts">
import Fa from "svelte-fa";
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
export let icon: IconDefinition;
export let title: string;
export let usageValue: string | null = null;
</script>
<div class="panel-header">
<Fa {icon} />
<h3>{title}</h3>
{#if usageValue}
<div class="usage-pill">{usageValue}</div>
{/if}
</div>
<style>
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface0);
flex-shrink: 0;
}
.panel-header h3 {
font-size: 0.8rem;
font-weight: 600;
margin: 0;
color: var(--text);
}
.panel-header :global(svg) {
color: var(--blue);
width: 0.8rem;
height: 0.8rem;
}
.usage-pill {
margin-left: auto;
background: var(--surface0);
padding: 0.15rem 0.5rem;
border-radius: 1rem;
font-size: 0.7rem;
font-weight: 500;
}
</style>
================================================
FILE: src/lib/components/stats/ProgressBar.svelte
================================================
<script lang="ts">
export let label: string;
export let value: number;
export let labelWidth = "2.5rem";
export let valueWidth = "2.5rem";
function getUsageClass(usage: number): string {
if (usage > 90) return "critical";
if (usage > 75) return "high";
if (usage > 50) return "medium";
return "low";
}
</script>
<div
class="progress-container"
style="--label-width: {labelWidth}; --value-width: {valueWidth}"
>
<span class="label">{label}</span>
<div class="bar-container">
<div
class="usage-bar {getUsageClass(value)}"
style="transform: translateX({value - 100}%);"
></div>
</div>
<span class="value">{Math.round(value)}%</span>
</div>
<style>
.progress-container {
width: 100%;
display: grid;
grid-template-columns: var(--label-width) 1fr var(--value-width);
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
}
.label {
color: var(--subtext0);
white-space: nowrap;
}
.value {
color: var(--text);
text-align: right;
white-space: nowrap;
}
.bar-container {
height: 8px;
background: var(--surface0);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.usage-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
transition: transform 0.3s ease;
transform-origin: left;
}
.usage-bar.low {
background: var(--blue);
}
.usage-bar.medium {
background: var(--yellow);
}
.usage-bar.high {
background: var(--peach);
}
.usage-bar.critical {
background: var(--red);
}
</style>
================================================
FILE: src/lib/components/stats/StatItem.svelte
================================================
<script lang="ts">
export let label: string;
export let value: string;
</script>
<div class="stat-item">
<span>{label}</span>
<span>{value}</span>
</div>
<style>
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
line-height: 1.2;
margin: 0;
padding: 0;
}
.stat-item span:first-child {
color: var(--subtext0);
}
.stat-item span:last-child {
color: var(--text);
font-weight: 500;
}
</style>
================================================
FILE: src/lib/components/stats/StatPanel.svelte
================================================
<script lang="ts">
export let title: string;
export let flex = 1;
</script>
<div class="stat-panel" style="--flex: {flex}">
<h3 class="panel-title">{title}</h3>
<div class="panel-content">
<slot />
</div>
</div>
<style>
.stat-panel {
flex: var(--flex);
min-width: 125px;
background: var(--mantle);
border-radius: 8px;
padding: 12px;
}
.panel-title {
font-size: 12px;
font-weight: 600;
color: var(--subtext0);
margin: 0 0 12px 0;
}
.panel-content {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
================================================
FILE: src/lib/components/stats/StatsBar.svelte
================================================
<script lang="ts">
import type { SystemStats } from "$lib/types";
import {
CpuPanel,
MemoryPanel,
StoragePanel,
SystemPanel,
NetworkPanel,
} from "$lib/components";
export let systemStats: SystemStats | null = null;
</script>
<div class="dashboard-stats">
{#if systemStats}
<div class="stats-layout">
<CpuPanel cpuUsage={systemStats.cpu_usage} />
<MemoryPanel
memoryTotal={systemStats.memory_total}
memoryUsed={systemStats.memory_used}
memoryFree={systemStats.memory_free}
/>
<StoragePanel
diskTotalBytes={systemStats.disk_total_bytes}
diskUsedBytes={systemStats.disk_used_bytes}
diskFreeBytes={systemStats.disk_free_bytes}
/>
<SystemPanel uptime={systemStats.uptime} loadAvg={systemStats.load_avg} />
<NetworkPanel
networkRxBytes={systemStats.network_rx_bytes}
networkTxBytes={systemStats.network_tx_bytes}
/>
</div>
{/if}
</div>
<style>
.dashboard-stats {
padding: 0.5rem;
overflow-x: auto;
}
.stats-layout {
display: flex;
gap: 0.75rem;
width: 100%;
}
</style>
================================================
FILE: src/lib/components/stats/StoragePanel.svelte
================================================
<script lang="ts">
import { faHardDrive } from "@fortawesome/free-solid-svg-icons";
import { PanelHeader, StatItem } from "$lib/components";
import { formatBytes, formatPercentage } from "$lib/utils";
export let diskTotalBytes: number;
export let diskUsedBytes: number;
export let diskFreeBytes: number;
$: diskUsagePercentage = (diskUsedBytes / diskTotalBytes) * 100;
</script>
<div class="stat-panel">
<PanelHeader
icon={faHardDrive}
title="Storage"
usageValue={formatPercentage(diskUsagePercentage)}
/>
<div class="stats-content">
<StatItem label="Total" value={formatBytes(diskTotalBytes)} />
<StatItem label="Used" value={formatBytes(diskUsedBytes)} />
<StatItem label="Free" value={formatBytes(diskFreeBytes)} />
</div>
</div>
<style>
.stat-panel {
flex: 0.8;
min-width: 125px;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
</style>
================================================
FILE: src/lib/components/stats/SystemPanel.svelte
================================================
<script lang="ts">
import { faServer } from "@fortawesome/free-solid-svg-icons";
import { PanelHeader, StatItem } from "$lib/components";
import { formatUptime } from "$lib/utils";
export let uptime: number;
export let loadAvg: [number, number, number];
</script>
<div class="stat-panel">
<PanelHeader icon={faServer} title="System" />
<div class="system-grid">
<StatItem label="Uptime" value={formatUptime(uptime)} />
<StatItem label="1m Load" value={loadAvg[0].toFixed(2)} />
<StatItem label="5m Load" value={loadAvg[1].toFixed(2)} />
<StatItem label="15m Load" value={loadAvg[2].toFixed(2)} />
</div>
</div>
<style>
.stat-panel {
flex: 0.8;
min-width: 125px;
background-color: var(--mantle);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
}
.system-grid {
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
</style>
================================================
FILE: src/lib/components/stats/index.ts
================================================
export { default as StatsBar } from "./StatsBar.svelte";
export { default as CpuPanel } from "./CpuPanel.svelte";
export { default as MemoryPanel } from "./MemoryPanel.svelte";
export { default as StoragePanel } from "./StoragePanel.svelte";
export { default as SystemPanel } from "./SystemPanel.svelte";
export { default as NetworkPanel } from "./NetworkPanel.svelte";
export { default as PanelHeader } from "./PanelHeader.svelte";
export { default as ProgressBar } from "./ProgressBar.svelte";
export { default as StatItem } from "./StatItem.svelte";
================================================
FILE: src/lib/components/toolbar/ColumnToggle.svelte
================================================
<script lang="ts">
import Fa from "svelte-fa";
import {
faChevronDown,
faChevronRight,
faChevronLeft,
} from "@fortawesome/free-solid-svg-icons";
import { settingsStore } from "$lib/stores/index";
import { overlayStore } from "$lib/stores/overlay";
import { onDestroy } from "svelte";
export let columns: Array<{
id: string;
label: string;
visible: boolean;
required?: boolean;
}>;
let containerElement: HTMLDivElement;
let overlayElement: HTMLDivElement;
let optionsContainer: HTMLDivElement;
let canScrollLeft = false;
let canScrollRight = false;
$: showColumnMenu = $overlayStore === "columns";
function handleColumnVisibilityChange(columnId: string, visible: boolean) {
settingsStore.updateConfig({
appearance: {
columnVisibility: {
...$settingsStore.appearance.columnVisibility,
[columnId]: visible,
},
},
});
}
function updateOverlayPosition() {
if (overlayElement && containerElement) {
const toolbarContent = containerElement.closest(".toolbar-content");
if (toolbarContent) {
const toolbarRect = toolbarContent.getBoundingClientRect();
const containerRect = containerElement.getBoundingClientRect();
const leftOffset = containerRect.left - toolbarRect.left;
const rightOffset = toolbarRect.right - containerRect.right;
const topOffset = containerRect.top - toolbarRect.top;
overlayElement.style.left = `-${leftOffset}px`;
overlayElement.style.right = `-${rightOffset}px`;
overlayElement.style.top = `-${topOffset}px`;
}
}
}
function updateScrollButtons() {
if (optionsContainer) {
canScrollLeft = optionsContainer.scrollLeft > 0;
canScrollRight =
optionsContainer.scrollLeft <
optionsContainer.scrollWidth - optionsContainer.clientWidth;
}
}
function scrollLeft() {
if (optionsContainer) {
optionsContainer.scrollBy({ left: -200, behavior: "smooth" });
setTimeout(updateScrollButtons, 100);
}
}
function scrollRight() {
if (optionsContainer) {
optionsContainer.scrollBy({ left: 200, behavior: "smooth" });
setTimeout(updateScrollButtons, 100);
}
}
function toggleExpanded(event: Event) {
event.stopPropagation();
if (showColumnMenu) {
overlayStore.close();
} else {
overlayStore.open("columns");
setTimeout(() => {
updateOverlayPosition();
updateScrollButtons();
}, 0);
}
}
function handleClickOutside(event: MouseEvent) {
if (
showColumnMenu &&
containerElement &&
!containerElement.contains(event.target as Node)
) {
overlayStore.close();
}
}
function setupClickOutside() {
if (typeof document !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
}
function cleanupClickOutside() {
if (typeof document !== "undefined") {
document.removeEventListener("click", handleClickOutside);
}
}
$: if (showColumnMenu) {
setTimeout(setupClickOutside, 0);
} else {
cleanupClickOutside();
}
onDestroy(() => {
cleanupClickOutside();
});
</script>
<div class="column-toggle" bind:this={containerElement}>
<button
class="touchbar-trigger"
class:active={showColumnMenu}
on:click={toggleExpanded}
aria-label="Toggle columns"
>
Columns
<span class="icon">
<Fa icon={showColumnMenu ? faChevronDown : faChevronRight} />
</span>
</button>
{#if showColumnMenu}
<div
class="touchbar-full-overlay"
bind:this={overlayElement}
on:click={() => overlayStore.close()}
on:keydown={(e) => e.key === "Escape" && overlayStore.close()}
role="dialog"
aria-label="Column visibility options"
tabindex="-1"
>
{#if canScrollLeft}
<button
class="scroll-chevron scroll-left"
on:click|stopPropagation={scrollLeft}
>
<Fa icon={faChevronLeft} />
</button>
{/if}
<div
class="touchbar-horizontal-options"
bind:this={optionsContainer}
on:scroll={updateScrollButtons}
>
{#each columns as column}
<button
class="touchbar-option"
class:active={column.visible}
class:disabled={column.required}
on:click|stopPropagation={() =>
!column.required &&
handleColumnVisibilityChange(column.id, !column.visible)}
title={column.required
? "Required column"
: `Toggle ${column.label}`}
>
{column.label}
</button>
{/each}
</div>
{#if canScrollRight}
<button
class="scroll-chevron scroll-right"
on:click|stopPropagation={scrollRight}
>
<Fa icon={faChevronRight} />
</button>
{/if}
</div>
{/if}
</div>
<style>
.column-toggle {
position: relative;
flex: 1;
}
.touchbar-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
}
.touchbar-trigger:hover {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-trigger.active {
background: var(--surface1);
border-color: var(--blue);
}
.icon {
font-size: 10px;
color: var(--subtext0);
transition: transform 0.2s ease;
}
.touchbar-full-overlay {
position: absolute;
top: -0px;
height: 44px;
background: var(--mantle);
border: none;
border-radius: 0;
box-shadow: none;
z-index: 1000;
display: flex;
align-items: center;
padding: 0 8px;
gap: 8px;
}
.scroll-chevron {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
flex-shrink: 0;
font-size: 10px;
animation: optionSlideIn 0.2s ease-out;
animation-fill-mode: both;
animation-delay: 0ms;
}
.scroll-chevron:hover {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-horizontal-options {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 0 4px;
}
.touchbar-horizontal-options::-webkit-scrollbar {
display: none;
}
.touchbar-option {
padding: 0 12px;
height: 28px;
font-size: 11px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
min-width: fit-content;
box-sizing: border-box;
animation: optionSlideIn 0.2s ease-out;
animation-fill-mode: both;
}
.touchbar-option:nth-child(1) {
animation-delay: 20ms;
}
.touchbar-option:nth-child(2) {
animation-delay: 40ms;
}
.touchbar-option:nth-child(3) {
animation-delay: 60ms;
}
.touchbar-option:nth-child(4) {
animation-delay: 80ms;
}
.touchbar-option:nth-child(5) {
animation-delay: 100ms;
}
.touchbar-option:nth-child(6) {
animation-delay: 120ms;
}
.touchbar-option:nth-child(7) {
animation-delay: 140ms;
}
.touchbar-option:nth-child(8) {
animation-delay: 160ms;
}
.touchbar-option:nth-child(9) {
animation-delay: 180ms;
}
.touchbar-option:nth-child(10) {
animation-delay: 200ms;
}
@keyframes optionSlideIn {
from {
opacity: 0;
transform: translateY(-8px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.touchbar-option:hover:not(.disabled) {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-option.active {
background: var(--blue);
color: var(--base);
border-color: var(--blue);
}
.touchbar-option.disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
================================================
FILE: src/lib/components/toolbar/FilterToggle.svelte
================================================
<script lang="ts">
import Fa from "svelte-fa";
import { faFilter } from "@fortawesome/free-solid-svg-icons";
import { overlayStore } from "$lib/stores/overlay";
import { onDestroy } from "svelte";
export let filters: {
cpu: { operator: string; value: number; enabled: boolean };
ram: { operator: string; value: number; enabled: boolean };
runtime: { operator: string; value: number; enabled: boolean };
status: { values: string[]; enabled: boolean };
} = {
cpu: { operator: ">", value: 50, enabled: false },
ram: { operator: ">", value: 100, enabled: false },
runtime: { operator: ">", value: 60, enabled: false },
status: { values: [], enabled: false },
};
let containerElement: HTMLDivElement;
let overlayElement: HTMLDivElement;
$: showFilters = $overlayStore === "filters";
$: hasActiveFilters = Object.values(filters).some((f) => f.enabled);
$: activeFilterCount = Object.values(filters).filter((f) => f.enabled).length;
const operators = [
{ value: ">", label: ">" },
{ value: "<", label: "<" },
];
const statusOptions = [
{ value: "Running", label: "Running", color: "var(--green)" },
{ value: "Sleeping", label: "Sleeping", color: "var(--blue)" },
{ value: "Stopped", label: "Stopped", color: "var(--red)" },
{ value: "Zombie", label: "Zombie", color: "var(--yellow)" },
];
function updateOverlayPosition() {
if (overlayElement && containerElement) {
const toolbarContent = containerElement.closest(".toolbar-content");
if (toolbarContent) {
const toolbarRect = toolbarContent.getBoundingClientRect();
const containerRect = containerElement.getBoundingClientRect();
const leftOffset = containerRect.left - toolbarRect.left;
const rightOffset = toolbarRect.right - containerRect.right;
const topOffset = containerRect.top - toolbarRect.top;
overlayElement.style.left = `-${leftOffset}px`;
overlayElement.style.right = `-${rightOffset}px`;
overlayElement.style.top = `-${topOffset}px`;
}
}
}
function toggleFilters(event: Event) {
event.stopPropagation();
if (showFilters) {
overlayStore.close();
} else {
overlayStore.open("filters");
setTimeout(updateOverlayPosition, 0);
}
}
function toggleFilter(type: keyof typeof filters) {
filters[type].enabled = !filters[type].enabled;
filters = { ...filters };
}
function updateFilter(
type: keyof typeof filters,
field: string,
value: string | number,
) {
if (type === "status" && field === "values") {
// Handle status array toggle
const statusValue = value as string;
const currentValues = filters.status.values;
if (currentValues.includes(statusValue)) {
filters.status.values = currentValues.filter((v) => v !== statusValue);
} else {
filters.status.values = [...currentValues, statusValue];
}
filters.status.enabled = filters.status.values.length > 0;
} else if (field === "operator") {
(filters[type as keyof Omit<typeof filters, "status">] as any)[field] =
value as string;
} else if (field === "value") {
(filters[type as keyof Omit<typeof filters, "status">] as any)[field] =
value as number;
}
filters = { ...filters };
}
function clearAllFilters() {
Object.keys(filters).forEach((key) => {
const filterKey = key as keyof typeof filters;
if (filterKey === "status") {
filters[filterKey].values = [];
}
filters[filterKey].enabled = false;
});
filters = { ...filters };
}
function getFilterLabel(type: keyof typeof filters): string {
const labels = {
cpu: "CPU %",
ram: "RAM MB",
runtime: "Runtime min",
status: "Status",
};
return labels[type];
}
function handleClickOutside(event: MouseEvent) {
if (
showFilters &&
containerElement &&
!containerElement.contains(event.target as Node)
) {
overlayStore.close();
}
}
function setupClickOutside() {
if (typeof document !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
}
function cleanupClickOutside() {
if (typeof document !== "undefined") {
document.removeEventListener("click", handleClickOutside);
}
}
$: if (showFilters) {
setTimeout(setupClickOutside, 0);
} else {
cleanupClickOutside();
}
onDestroy(() => {
cleanupClickOutside();
});
</script>
<div class="filter-toggle" bind:this={containerElement}>
<button
class="filter-button"
class:active={showFilters}
class:has-filters={hasActiveFilters}
on:click={toggleFilters}
aria-label="Toggle filters"
>
<Fa icon={faFilter} />
Filters
{#if hasActiveFilters}
<span class="filter-count">{activeFilterCount}</span>
{/if}
</button>
{#if showFilters}
<div
class="touchbar-full-overlay"
bind:this={overlayElement}
on:click={() => overlayStore.close()}
on:keydown={(e) => e.key === "Escape" && overlayStore.close()}
role="dialog"
aria-label="Filter options overlay"
tabindex="-1"
>
<div class="filter-content">
<div class="filter-sections">
<!-- Numeric Filters -->
<div class="filter-section">
<span class="section-label">Performance:</span>
<div class="filter-controls">
{#each [["cpu", "CPU %"], ["ram", "RAM MB"], ["runtime", "Runtime min"]] as [type, label]}
{@const filterKey = type as "cpu" | "ram" | "runtime"}
<div class="filter-control">
<button
class="filter-toggle-btn"
class:active={filters[filterKey].enabled}
on:click|stopPropagation={() => toggleFilter(filterKey)}
>
{label}
</button>
{#if filters[filterKey].enabled}
<select
class="operator-select"
bind:value={filters[filterKey].operator}
on:change={(e) =>
updateFilter(
filterKey,
"operator",
(e.target as HTMLSelectElement).value,
)}
on:click|stopPropagation
>
{#each operators as op}
<option value={op.value}>{op.label}</option>
{/each}
</select>
<input
type="number"
class="value-input"
bind:value={filters[filterKey].value}
on:input={(e) =>
updateFilter(
filterKey,
"value",
parseInt((e.target as HTMLInputElement).value),
)}
on:click|stopPropagation
on:focus|stopPropagation
placeholder={type === "cpu"
? "50"
: type === "ram"
? "100"
: "60"}
/>
{#if type === "ram"}
<span class="unit">MB</span>
{:else if type === "runtime"}
<span class="unit">min</span>
{:else}
<span class="unit">%</span>
{/if}
{/if}
</div>
{/each}
</div>
</div>
<!-- Status Filter -->
<div class="filter-section">
<span class="section-label">Status:</span>
<div class="status-controls">
{#each statusOptions as status}
<button
class="status-toggle"
class:active={filters.status.values.includes(status.value)}
style="--status-color: {status.color}"
on:click|stopPropagation={() =>
updateFilter("status", "values", status.value)}
>
{status.label}
</button>
{/each}
</div>
</div>
<!-- Actions -->
<div class="filter-actions">
{#if hasActiveFilters}
<button
class="clear-all-btn"
on:click|stopPropagation={clearAllFilters}
>
<Fa icon={faFilter} />
Clear All
</button>
{/if}
<div class="filter-summary">
{#if hasActiveFilters}
<span
>{activeFilterCount} filter{activeFilterCount > 1 ? "s" : ""} active</span
>
{:else}
<span>No filters applied</span>
{/if}
</div>
</div>
</div>
</div>
</div>
{/if}
</div>
<style>
.filter-toggle {
position: relative;
}
.filter-button {
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
}
.filter-button:hover {
background: var(--surface1);
border-color: var(--blue);
}
.filter-button.active {
background: var(--surface1);
border-color: var(--blue);
}
.filter-button.has-filters {
border-color: var(--blue);
background: var(--surface1);
}
.filter-count {
background: var(--blue);
color: var(--base);
border-radius: 10px;
padding: 2px 6px;
font-size: 10px;
font-weight: 600;
min-width: 16px;
text-align: center;
}
.touchbar-full-overlay {
position: absolute;
top: -0px;
height: 44px;
background: var(--mantle);
border: none;
border-radius: 0;
box-shadow: none;
z-index: 1000;
display: flex;
align-items: center;
padding: 0 16px;
gap: 16px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.touchbar-full-overlay::-webkit-scrollbar {
display: none;
}
.filter-content {
display: flex;
align-items: center;
gap: 32px;
width: 100%;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filter-sections {
display: flex;
align-items: center;
gap: 32px;
flex: 1;
}
.filter-section {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.section-label {
font-size: 12px;
font-weight: 500;
color: var(--subtext0);
flex-shrink: 0;
}
.filter-controls {
display: flex;
gap: 8px;
align-items: center;
}
.filter-control {
display: flex;
align-items: center;
gap: 4px;
}
.filter-toggle-btn {
padding: 4px 8px;
height: 26px;
font-size: 11px;
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 4px;
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.filter-toggle-btn:hover {
background: var(--surface1);
border-color: var(--blue);
}
.filter-toggle-btn.active {
background: var(--blue);
color: var(--base);
border-color: var(--blue);
}
.operator-select {
padding: 4px 8px;
height: 26px;
font-size: 11px;
border: 1px solid var(--surface1);
border-radius: 4px;
background: var(--surface0);
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
text-align: center;
min-width: 32px;
}
.operator-select:hover {
background: var(--surface1);
border-color: var(--blue);
}
.operator-select:focus {
outline: none;
border-color: var(--blue);
background: var(--surface1);
}
.value-input {
width: 50px;
padding: 2px 6px;
height: 22px;
font-size: 11px;
border: 1px solid var(--surface1);
border-radius: 3px;
background: var(--surface0);
color: var(--text);
text-align: center;
}
.value-input:focus {
outline: none;
border-color: var(--blue);
}
.unit {
font-size: 10px;
color: var(--subtext0);
}
.status-controls {
display: flex;
gap: 6px;
align-items: center;
}
.status-toggle {
padding: 4px 8px;
height: 26px;
font-size: 11px;
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 4px;
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.status-toggle:hover {
background: var(--surface1);
border-color: var(--status-color);
}
.status-toggle.active {
background: var(--status-color);
color: var(--base);
border-color: var(--status-color);
}
.filter-actions {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.clear-all-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
height: 26px;
font-size: 11px;
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 4px;
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
}
.clear-all-btn:hover {
background: var(--red);
border-color: var(--red);
color: var(--base);
}
.filter-summary {
font-size: 11px;
color: var(--subtext0);
}
</style>
================================================
FILE: src/lib/components/toolbar/PaginationControls.svelte
================================================
<script lang="ts">
import type { AppConfig } from "$lib/types";
import { settingsStore } from "$lib/stores/index";
import { overlayStore } from "$lib/stores/overlay";
import { ITEMS_PER_PAGE_OPTIONS } from "$lib/constants";
import { onDestroy } from "svelte";
export let itemsPerPage: number;
export let currentPage: number;
export let totalPages: number;
export let totalResults: number;
let containerElement: HTMLDivElement;
let overlayElement: HTMLDivElement;
$: isExpanded = $overlayStore === "pagination";
function changePage(page: number) {
if (page >= 1 && page <= totalPages) {
currentPage = page;
}
}
function updateBehaviorConfig(key: keyof AppConfig["behavior"], value: any) {
settingsStore.updateConfig({
behavior: {
...$settingsStore.behavior,
[key]: value,
},
});
}
function selectItemsPerPage(value: number) {
itemsPerPage = value;
updateBehaviorConfig("itemsPerPage", itemsPerPage);
overlayStore.close();
}
function updateOverlayPosition() {
if (overlayElement && containerElement) {
const toolbarContent = containerElement.closest(".toolbar-content");
if (toolbarContent) {
const toolbarRect = toolbarContent.getBoundingClientRect();
const containerRect = containerElement.getBoundingClientRect();
const leftOffset = containerRect.left - toolbarRect.left;
const rightOffset = toolbarRect.right - containerRect.right;
const topOffset = containerRect.top - toolbarRect.top;
overlayElement.style.left = `-${leftOffset}px`;
overlayElement.style.right = `-${rightOffset}px`;
overlayElement.style.top = `-${topOffset}px`;
}
}
}
function toggleExpanded(event: Event) {
event.stopPropagation();
if (isExpanded) {
overlayStore.close();
} else {
overlayStore.open("pagination");
setTimeout(updateOverlayPosition, 0);
}
}
function handleClickOutside(event: MouseEvent) {
if (
isExpanded &&
containerElement &&
!containerElement.contains(event.target as Node)
) {
overlayStore.close();
}
}
function setupClickOutside() {
if (typeof document !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
}
function cleanupClickOutside() {
if (typeof document !== "undefined") {
document.removeEventListener("click", handleClickOutside);
}
}
$: if (isExpanded) {
setTimeout(setupClickOutside, 0);
} else {
cleanupClickOutside();
}
onDestroy(() => {
cleanupClickOutside();
});
</script>
<div class="pagination-controls">
<div
class="pagination-per-page"
class:active={isExpanded}
bind:this={containerElement}
>
<button
class="touchbar-trigger"
class:active={isExpanded}
on:click={toggleExpanded}
>
{itemsPerPage} per page
</button>
{#if isExpanded}
<div
class="touchbar-full-overlay"
bind:this={overlayElement}
on:click={() => overlayStore.close()}
on:keydown={(e) => e.key === "Escape" && overlayStore.close()}
role="dialog"
aria-label="Pagination controls"
tabindex="-1"
>
<div class="touchbar-horizontal-options">
{#each ITEMS_PER_PAGE_OPTIONS as option}
<button
class="touchbar-option"
class:active={option === itemsPerPage}
on:click|stopPropagation={() => selectItemsPerPage(option)}
>
{option}
</button>
{/each}
</div>
</div>
{/if}
</div>
<div class="pagination">
<button
class="btn-page"
disabled={currentPage === 1}
on:click={() => changePage(1)}
>
««
</button>
<button
class="btn-page"
disabled={currentPage === 1}
on:click={() => changePage(currentPage - 1)}
>
«
</button>
<div class="page-info">
<span>Page {currentPage} of {totalPages}</span>
<span class="results-info">({totalResults} processes)</span>
</div>
<button
class="btn-page"
disabled={currentPage === totalPages}
on:click={() => changePage(currentPage + 1)}
>
»
</button>
<button
class="btn-page"
disabled={currentPage === totalPages}
on:click={() => changePage(totalPages)}
>
»»
</button>
</div>
</div>
<style>
.pagination-controls {
display: flex;
align-items: center;
gap: 12px;
}
.pagination-per-page {
display: flex;
align-items: center;
position: relative;
}
.touchbar-trigger {
padding: 6px 12px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
display: flex;
align-items: center;
box-sizing: border-box;
}
.touchbar-trigger:hover {
background-color: var(--surface1);
border-color: var(--blue);
}
.touchbar-trigger.active {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-full-overlay {
position: absolute;
top: -0px;
height: 44px;
background: var(--mantle);
border: none;
border-radius: 0;
box-shadow: none;
z-index: 1000;
display: flex;
align-items: center;
padding: 0 12px;
gap: 12px;
}
.touchbar-horizontal-options {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.touchbar-horizontal-options::-webkit-scrollbar {
display: none;
}
.touchbar-option {
padding: 0 12px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
min-width: fit-content;
box-sizing: border-box;
animation: optionSlideIn 0.2s ease-out;
animation-fill-mode: both;
}
.touchbar-option:nth-child(1) {
animation-delay: 0ms;
}
.touchbar-option:nth-child(2) {
animation-delay: 40ms;
}
.touchbar-option:nth-child(3) {
animation-delay: 80ms;
}
.touchbar-option:nth-child(4) {
animation-delay: 120ms;
}
@keyframes optionSlideIn {
from {
opacity: 0;
transform: translateY(-8px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.touchbar-option:hover:not(.disabled) {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-option.active {
background: var(--blue);
color: var(--base);
border-color: var(--blue);
}
.pagination {
display: flex;
align-items: center;
gap: 8px;
}
.btn-page {
padding: 6px 10px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-page:hover:not(:disabled) {
background: var(--surface1);
}
.btn-page:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
font-size: 12px;
color: var(--subtext0);
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.page-info span {
display: block;
}
.results-info {
color: var(--overlay0);
}
</style>
================================================
FILE: src/lib/components/toolbar/RefreshControls.svelte
================================================
<script lang="ts">
import Fa from "svelte-fa";
import { faPlay, faPause } from "@fortawesome/free-solid-svg-icons";
import type { AppConfig } from "$lib/types";
import { settingsStore } from "$lib/stores/index";
import { overlayStore } from "$lib/stores/overlay";
import { REFRESH_RATE_OPTIONS } from "$lib/constants";
import { onDestroy } from "svelte";
export let refreshRate: number;
export let isFrozen: boolean;
let containerElement: HTMLDivElement;
let overlayElement: HTMLDivElement;
$: isExpanded = $overlayStore === "refresh";
function updateBehaviorConfig(key: keyof AppConfig["behavior"], value: any) {
settingsStore.updateConfig({
behavior: {
...$settingsStore.behavior,
[key]: value,
},
});
}
function selectRefreshRate(value: number) {
refreshRate = value;
updateBehaviorConfig("refreshRate", refreshRate);
overlayStore.close();
}
function getCurrentLabel() {
return (
REFRESH_RATE_OPTIONS.find((opt) => opt.value === refreshRate)?.label ||
"1s"
);
}
function updateOverlayPosition() {
if (overlayElement && containerElement) {
const toolbarContent = containerElement.closest(".toolbar-content");
if (toolbarContent) {
const toolbarRect = toolbarContent.getBoundingClientRect();
const containerRect = containerElement.getBoundingClientRect();
const leftOffset = containerRect.left - toolbarRect.left;
const rightOffset = toolbarRect.right - containerRect.right;
const topOffset = containerRect.top - toolbarRect.top;
overlayElement.style.left = `-${leftOffset}px`;
overlayElement.style.right = `-${rightOffset}px`;
overlayElement.style.top = `-${topOffset}px`;
}
}
}
function toggleExpanded(event: Event) {
if (!isFrozen) {
event.stopPropagation();
if (isExpanded) {
overlayStore.close();
} else {
overlayStore.open("refresh");
setTimeout(updateOverlayPosition, 0);
}
}
}
function handleClickOutside(event: MouseEvent) {
if (
isExpanded &&
containerElement &&
!containerElement.contains(event.target as Node)
) {
overlayStore.close();
}
}
function setupClickOutside() {
if (typeof document !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
}
function cleanupClickOutside() {
if (typeof document !== "undefined") {
document.removeEventListener("click", handleClickOutside);
}
}
$: if (isExpanded) {
setTimeout(setupClickOutside, 0);
} else {
cleanupClickOutside();
}
onDestroy(() => {
cleanupClickOutside();
});
</script>
<div class="refresh-controls">
<div
class="refresh-rate"
class:active={isExpanded}
bind:this={containerElement}
>
<button
class="touchbar-trigger"
class:disabled={isFrozen}
class:active={isExpanded}
on:click={toggleExpanded}
>
{getCurrentLabel()}
</button>
{#if isExpanded}
<div
class="touchbar-full-overlay"
bind:this={overlayElement}
on:click={() => overlayStore.close()}
on:keydown={(e) => e.key === "Escape" && overlayStore.close()}
role="dialog"
aria-label="Refresh rate controls"
tabindex="-1"
>
<div class="touchbar-horizontal-options">
{#each REFRESH_RATE_OPTIONS as option}
<button
class="touchbar-option"
class:active={option.value === refreshRate}
on:click|stopPropagation={() => selectRefreshRate(option.value)}
>
{option.label}
</button>
{/each}
</div>
</div>
{/if}
</div>
<button
class="btn-action"
class:frozen={isFrozen}
on:click={() => (isFrozen = !isFrozen)}
title={isFrozen ? "Resume Updates" : "Pause Updates"}
>
{#if isFrozen}
<Fa icon={faPlay} color="var(--red)" />
{:else}
<Fa icon={faPause} color="var(--subtext0)" />
{/if}
</button>
</div>
<style>
.refresh-controls {
display: flex;
gap: 8px;
align-items: center;
}
.refresh-controls :global(svg) {
font-size: 14px;
color: var(--subtext0);
}
.refresh-rate {
display: flex;
align-items: center;
position: relative;
}
.touchbar-trigger {
height: 28px;
padding: 0 12px;
border: 1px solid var(--surface1);
border-radius: 6px;
background: var(--surface0);
color: var(--text);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.touchbar-trigger:hover:not(.disabled) {
background-color: var(--surface1);
border-color: var(--blue);
}
.touchbar-trigger.active {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-trigger.disabled {
opacity: 0.7;
cursor: not-allowed;
}
.touchbar-full-overlay {
position: absolute;
top: -0px;
height: 44px;
background: var(--mantle);
border: none;
border-radius: 0;
box-shadow: none;
z-index: 1000;
display: flex;
align-items: center;
padding: 0 12px;
gap: 12px;
}
.touchbar-horizontal-options {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.touchbar-horizontal-options::-webkit-scrollbar {
display: none;
}
.touchbar-option {
padding: 0 12px;
height: 28px;
font-size: 12px;
color: var(--text);
background: var(--surface0);
border: 1px solid var(--surface1);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
min-width: fit-content;
box-sizing: border-box;
animation: optionSlideIn 0.2s ease-out;
animation-fill-mode: both;
}
.touchbar-option:nth-child(1) {
animation-delay: 0ms;
}
.touchbar-option:nth-child(2) {
animation-delay: 40ms;
}
.touchbar-option:nth-child(3) {
animation-delay: 80ms;
}
.touchbar-option:nth-child(4) {
animation-delay: 120ms;
}
@keyframes optionSlideIn {
from {
opacity: 0;
transform: translateY(-8px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.touchbar-option:hover:not(.disabled) {
background: var(--surface1);
border-color: var(--blue);
}
.touchbar-option.active {
background: var(--blue);
color: var(--base);
border-color: var(--blue);
}
.btn-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: var(--surface0);
border: 1px solid var(--surface1);
color: var(--text);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-action:hover {
background: var(--surface1);
}
.btn-action.frozen {
background: var(--yellow);
}
</style>
================================================
FILE: src/lib/components/toolbar/SearchBox.svelte
================================================
<script lang="ts">
import { overlayStore } from "$lib/stores/overlay";
import { onDestroy, onMount } from "svelte";
export let searchTerm: string;
let containerElement: HTMLDivElement;
let overlayElement: HTMLDivElement;
let searchInputElement: HTMLInputElement;
let placeholderIndex = 0;
let placeholderInterval: NodeJS.Timeout;
$: showHelp = $overlayStore === "searchHelp";
$: hasActiveSearch = searchTerm.trim().length > 0;
const searchExamples = [
{
query: "systemd, dbus",
description: "Multiple terms (comma-separated)",
type: "multi",
},
{
query: "d$",
description: "Processes ending with 'd' (daemons)",
type: "regex",
},
{
query: "^kernel",
description: "Kernel processes",
type: "regex",
},
{
query: "ssh.*server",
description: "SSH server processes",
type: "regex",
},
{
query: "1234",
description: "Search by PID",
type: "pid",
},
{
query: "python, node, nginx",
description: "Find web/app server processes",
type: "multi",
},
{
query: "docker, containerd",
description: "Container processes",
type: "multi",
},
{
query: "gnome, plasma",
description: "Desktop environment processes",
type: "multi",
},
];
const placeholders = [
"Search processes...",
"Try: systemd, dbus",
"Try: d$ (daemons)",
"Try: ^kernel (regex)",
"Search by name, command, or PID",
"Try: docker, nginx",
];
function rotatePlaceholder() {
placeholderIndex = (placeholderIndex + 1) % placeholders.length;
}
function updateOverlayPosition() {
if (overlayElement && containerElement) {
const toolbarContent = containerElement.closest(".toolbar-content");
if (toolbarContent) {
const toolbarRect = toolbarContent.getBoundingClientRect();
const containerRect = containerElement.getBoundingClientRect();
const leftOffset = containerRect.left - toolbarRect.left;
const rightOffset = toolbarRect.right - containerRect.right;
const topOffset = containerRect.top - toolbarRect.top;
overlayElement.style.left = `-${leftOffset}px`;
overlayElement.style.right = `-${rightOffset}px`;
overlayElement.style.top = `-${topOffset}px`;
}
}
}
function handleFocus() {
overlayStore.open("searchHelp");
setTimeout(() => {
updateOverlayPosition();
// Focus the enhanced search input in the overlay
const overlayInput = overlayElement?.querySelector(
".overlay-search-input",
) as HTMLInputElement;
if (overlayInput) {
overlayInput.focus();
}
}, 0);
}
function handleBlur(event: FocusEvent) {
// Small delay to allow clicking on examples
setTimeout(() => {
const activeElement = document.activeElement;
if (!containerElement?.contains(activeElement as Node)) {
overlayStore.close();
}
}, 150);
}
function useExample(example: string) {
searchTerm = example;
// Keep focus on the overlay search input
const overlayInput = overlayElement?.querySelector(
".overlay-search-input",
) as HTMLInputElement;
if (overlayInput) {
overlayInput.focus();
// Position cursor at end
setTimeout(() => {
overlayInput.setSelectionRange(example.length, example.length);
}, 0);
}
}
function handleClickOutside(event: MouseEvent) {
if (
showHelp &&
containerElement &&
!containerElement.contains(event.target as Node)
) {
overlayStore.close();
}
}
function setupClickOutside() {
if (typeof document !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
}
gitextract_7hzxi6z1/ ├── .github/ │ ├── CODEOWNERS │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── pull_request_template.md │ └── 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 ├── docs/ │ ├── index.html │ ├── main.js │ └── styles.css ├── jsconfig.json ├── package.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 │ │ │ ├── FilterToggle.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 │ │ │ ├── overlay.ts │ │ │ ├── processes.ts │ │ │ ├── settings.ts │ │ │ └── theme.ts │ │ ├── types/ │ │ │ └── index.ts │ │ └── utils/ │ │ └── index.ts │ └── routes/ │ ├── +layout.js │ ├── +layout.svelte │ └── +page.svelte ├── src-tauri/ │ ├── .cargo/ │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities/ │ │ └── default.json │ ├── icons/ │ │ └── icon.icns │ ├── 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 ├── svelte.config.js └── vite.config.js
SYMBOL INDEX (72 symbols across 17 files)
FILE: docs/main.js
function setTheme (line 7) | function setTheme(isDark) {
function fetchDownloadStats (line 36) | async function fetchDownloadStats() {
function updateVersion (line 58) | async function updateVersion() {
function updateDownloadLinks (line 74) | function updateDownloadLinks(versionNumber) {
FILE: src-tauri/build.rs
function main (line 1) | fn main() {
FILE: src-tauri/src/commands.rs
function get_processes (line 29) | pub async fn get_processes(
function kill_process (line 64) | pub async fn kill_process(pid: u32, state: State<'_, AppState>) -> Resul...
FILE: src-tauri/src/main.rs
function main (line 23) | fn main() {
FILE: src-tauri/src/monitoring/process_monitor.rs
function os_string_vec_to_string_vec (line 13) | fn os_string_vec_to_string_vec(v: &[OsString]) -> Vec<String> {
type ProcessMonitor (line 21) | pub struct ProcessMonitor {
method new (line 28) | pub fn new() -> Self {
method collect_processes (line 43) | pub fn collect_processes(&mut self, sys: &sysinfo::System) -> Result<V...
method kill_process (line 59) | pub fn kill_process(sys: &sysinfo::System, pid: u32) -> bool {
method get_current_time (line 66) | fn get_current_time() -> Result<u64, String> {
method collect_process_data (line 74) | fn collect_process_data(&self, sys: &sysinfo::System, current_time: u6...
method build_process_info (line 108) | fn build_process_info(&mut self, processes: Vec<ProcessData>) -> Vec<P...
method format_status (line 144) | pub fn format_status(status: ProcessStatus) -> String {
function test_process_monitor_creation (line 162) | fn test_process_monitor_creation() {
function test_process_collection (line 169) | fn test_process_collection() {
FILE: src-tauri/src/monitoring/system_monitor.rs
type SystemMonitor (line 14) | pub struct SystemMonitor {
method new (line 25) | pub fn new(networks: &Networks) -> Self {
method collect_stats (line 46) | pub fn collect_stats(
method filter_disks (line 75) | fn filter_disks(disks: &[Disk]) -> impl Iterator<Item = &Disk> {
method filter_disks (line 83) | fn filter_disks(disks: &[Disk]) -> impl Iterator<Item = &Disk> {
method calculate_network_stats (line 88) | fn calculate_network_stats(&mut self, networks: &Networks) -> (u64, u6...
method calculate_disk_stats (line 108) | fn calculate_disk_stats(&self, disks: &Disks) -> (u64, u64, u64) {
function test_system_monitor_creation (line 126) | fn test_system_monitor_creation() {
function test_stats_collection (line 135) | fn test_stats_collection() {
FILE: src-tauri/src/monitoring/types.rs
type ProcessData (line 8) | pub(crate) struct ProcessData {
type ProcessStaticInfo (line 44) | pub struct ProcessStaticInfo {
type ProcessInfo (line 56) | pub struct ProcessInfo {
type SystemStats (line 94) | pub struct SystemStats {
FILE: src-tauri/src/state.rs
type AppState (line 14) | pub struct AppState {
method new (line 35) | pub fn new() -> Self {
FILE: src-tauri/src/ui/window.rs
function setup_window_effects (line 14) | pub fn setup_window_effects(window: &WebviewWindow) -> Result<(), Box<dy...
function setup_window_effects (line 21) | pub fn setup_window_effects(window: &WebviewWindow) -> Result<(), Box<dy...
function setup_window_effects (line 33) | pub fn setup_window_effects(_window: &WebviewWindow) -> Result<(), Box<d...
FILE: src/lib/constants/index.ts
constant ASCII_ART (line 1) | const ASCII_ART = `
constant APP_INFO (line 10) | const APP_INFO = {
constant ITEMS_PER_PAGE_OPTIONS (line 17) | const ITEMS_PER_PAGE_OPTIONS = [15, 25, 50, 100, 250, 500];
constant REFRESH_RATE_OPTIONS (line 19) | const REFRESH_RATE_OPTIONS = [
constant STATUS_OPTIONS (line 27) | const STATUS_OPTIONS = [
constant THEME_GROUPS (line 35) | const THEME_GROUPS = [
FILE: src/lib/definitions/settings.ts
constant DEFAULT_CONFIG (line 3) | const DEFAULT_CONFIG: AppConfig = {
FILE: src/lib/stores/overlay.ts
type OverlayType (line 3) | type OverlayType =
function createOverlayStore (line 13) | function createOverlayStore() {
FILE: src/lib/stores/processes.ts
type ProcessStore (line 5) | interface ProcessStore {
function createProcessStore (line 47) | function createProcessStore() {
FILE: src/lib/stores/settings.ts
function createSettingsStore (line 5) | function createSettingsStore() {
FILE: src/lib/stores/theme.ts
function createThemeStore (line 4) | function createThemeStore() {
function applyTheme (line 49) | function applyTheme(theme: Theme) {
FILE: src/lib/types/index.ts
type Process (line 2) | interface Process {
type SystemStats (line 21) | interface SystemStats {
type Column (line 36) | interface Column {
type Theme (line 44) | interface Theme {
type AppConfig (line 72) | interface AppConfig {
type ColumnDefinition (line 83) | interface ColumnDefinition {
type StatusOption (line 90) | interface StatusOption {
type RefreshRateOption (line 95) | interface RefreshRateOption {
type ToolBarProps (line 100) | interface ToolBarProps {
type SortConfig (line 112) | interface SortConfig {
FILE: src/lib/utils/index.ts
type ProcessStatus (line 4) | interface ProcessStatus {
function formatMemorySize (line 10) | function formatMemorySize(bytes: number): string {
function formatPercentage (line 15) | function formatPercentage(value: number): string {
function formatUptime (line 19) | function formatUptime(seconds: number): string {
function getUsageClass (line 26) | function getUsageClass(percentage: number): string {
function formatBytes (line 33) | function formatBytes(bytes: number): string {
function formatDate (line 46) | function formatDate(timestamp: number) {
function debounce (line 51) | function debounce<T extends (...args: any[]) => any>(
function filterProcesses (line 65) | function filterProcesses(
function compareValue (line 166) | function compareValue(
function sortProcesses (line 190) | function sortProcesses(
Condensed preview — 95 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (304K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 13,
"preview": "* @Abdenasser"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 885,
"preview": "# Contributing to NeoHtop\n\nThank you for considering contributing to NeoHtop! We welcome contributions from the communit"
},
{
"path": ".github/FUNDING.yml",
"chars": 19,
"preview": "github: abdenasser\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 635,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 181,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Support\n url: https://github.com/Abdenasser/neohtop/discussions\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 604,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
},
{
"path": ".github/pull_request_template.md",
"chars": 1244,
"preview": "## Description\n\nPlease include a summary of the changes and the related issue. Please also include relevant motivation a"
},
{
"path": ".github/workflows/build-check.yml",
"chars": 2743,
"preview": "name: Build Check\n\non:\n pull_request:\n branches: [main]\n paths:\n - \"src-tauri/**\"\n - \".github/workflows"
},
{
"path": ".github/workflows/format-check.yml",
"chars": 486,
"preview": "name: Format Check\n\non:\n pull_request:\n branches: [ main ]\n\njobs:\n format:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": ".github/workflows/linux-aarch64-nightly.yml",
"chars": 7340,
"preview": "name: Linux (aarch64) Nightly Build\n\non:\n workflow_dispatch:\n inputs:\n release_upload_url:\n description:"
},
{
"path": ".github/workflows/linux-x86_64-nightly.yml",
"chars": 4261,
"preview": "name: Linux (x86_64) Nightly Build\n\non:\n workflow_dispatch:\n inputs:\n release_upload_url:\n description: "
},
{
"path": ".github/workflows/macos-nightly.yml",
"chars": 4187,
"preview": "name: MacOS (Intel/Apple Silicon) Nightly Build\n\non:\n workflow_dispatch:\n inputs:\n release_upload_url:\n "
},
{
"path": ".github/workflows/test-release.yml",
"chars": 1464,
"preview": "name: Test Release Build\n\non:\n workflow_dispatch: # Manual trigger\n\njobs:\n create-draft:\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/windows-nightly.yml",
"chars": 2040,
"preview": "name: Windows (x86_64) Nightly Build\n\non:\n workflow_dispatch:\n inputs:\n release_upload_url:\n description"
},
{
"path": ".gitignore",
"chars": 158,
"preview": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\nvite.config.js.timestamp-*\nvite.config.ts."
},
{
"path": ".husky/pre-commit",
"chars": 74,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpm exec lint-staged\n"
},
{
"path": ".prettierrc",
"chars": 121,
"preview": "{\n \"plugins\": [\"prettier-plugin-svelte\"],\n \"overrides\": [{ \"files\": \"*.svelte\", \"options\": { \"parser\": \"svelte\" } }]\n}"
},
{
"path": ".vscode/extensions.json",
"chars": 120,
"preview": "{\n \"recommendations\": [\n \"svelte.svelte-vscode\",\n \"tauri-apps.tauri-vscode\",\n \"rust-lang.rust-analyzer\"\n ]\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 38,
"preview": "{\n \"svelte.enable-ts-plugin\": true\n}\n"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2024 Abdenasser\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 5738,
"preview": "\n<div align=\"center\">\n <img src=\"app-icon.png\" alt=\"NeoHtop Logo\" width=\"120\" />\n <h1>NeoHtop</h1>\n <p>A modern, cros"
},
{
"path": "docs/index.html",
"chars": 24418,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, i"
},
{
"path": "docs/main.js",
"chars": 5770,
"preview": "// ===============================\n// Theme Management\n// ===============================\nconst themeToggle = document.g"
},
{
"path": "docs/styles.css",
"chars": 25505,
"preview": ":root {\n --primary-color: #6366f1;\n --secondary-color: #818cf8;\n --background: #ffffff;\n --text-primary: #1f2937;\n "
},
{
"path": "jsconfig.json",
"chars": 675,
"preview": "{\n \"extends\": \"./.svelte-kit/tsconfig.json\",\n \"compilerOptions\": {\n \"allowJs\": true,\n \"checkJs\": true,\n \"esMo"
},
{
"path": "package.json",
"chars": 1512,
"preview": "{\n \"name\": \"neohtop\",\n \"version\": \"1.2.0\",\n \"description\": \"\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite de"
},
{
"path": "src/App.svelte",
"chars": 0,
"preview": ""
},
{
"path": "src/app.css",
"chars": 2779,
"preview": ":root {\n /* Default theme values will be overridden by theme store */\n --base: #1e1e2e;\n --mantle: #181825;\n --crust"
},
{
"path": "src/app.html",
"chars": 371,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"%sveltekit.assets%/fav"
},
{
"path": "src/lib/components/AppInfo.svelte",
"chars": 5852,
"preview": "<script lang=\"ts\">\n import { getVersion } from \"@tauri-apps/api/app\";\n import { onMount } from \"svelte\";\n import { Th"
},
{
"path": "src/lib/components/ThemeSwitcher.svelte",
"chars": 9789,
"preview": "<script lang=\"ts\">\n import { themeStore } from \"$lib/stores\";\n import { overlayStore } from \"$lib/stores/overlay\";\n i"
},
{
"path": "src/lib/components/TitleBar.svelte",
"chars": 846,
"preview": "<script lang=\"ts\">\n</script>\n\n<div class=\"title-bar\" data-tauri-drag-region>\n <div class=\"title\">\n <img src=\"/32x32."
},
{
"path": "src/lib/components/index.ts",
"chars": 284,
"preview": "export * from \"./toolbar\";\nexport * from \"./process\";\nexport * from \"./stats\";\nexport * from \"./modals\";\nexport { defaul"
},
{
"path": "src/lib/components/modals/KillProcessModal.svelte",
"chars": 2460,
"preview": "<script lang=\"ts\">\n import { Modal } from \"$lib/components\";\n\n interface Process {\n pid: number;\n name: string;\n"
},
{
"path": "src/lib/components/modals/Modal.svelte",
"chars": 1604,
"preview": "<script lang=\"ts\">\n export let show = false;\n export let maxWidth = \"600px\";\n export let title: string;\n export let "
},
{
"path": "src/lib/components/modals/ProcessDetailsModal.svelte",
"chars": 12721,
"preview": "<script lang=\"ts\">\n import { Modal } from \"$lib/components\";\n import { formatBytes } from \"$lib/utils\";\n import type "
},
{
"path": "src/lib/components/modals/index.ts",
"chars": 203,
"preview": "export { default as Modal } from \"./Modal.svelte\";\nexport { default as ProcessDetailsModal } from \"./ProcessDetailsModal"
},
{
"path": "src/lib/components/process/ActionButtons.svelte",
"chars": 3162,
"preview": "<script lang=\"ts\">\n import {\n faThumbtack,\n faInfoCircle,\n faXmark,\n } from \"@fortawesome/free-solid-svg-icon"
},
{
"path": "src/lib/components/process/ProcessIcon.svelte",
"chars": 2444,
"preview": "<script lang=\"ts\">\n import * as SimpleIcons from \"simple-icons\";\n\n export let processName: string;\n export let size: "
},
{
"path": "src/lib/components/process/ProcessRow.svelte",
"chars": 1814,
"preview": "<script lang=\"ts\">\n import type { Process, Column } from \"$lib/types\";\n import { ProcessIcon, ActionButtons } from \"$l"
},
{
"path": "src/lib/components/process/ProcessTable.svelte",
"chars": 1969,
"preview": "<script lang=\"ts\">\n import type { Process, Column } from \"$lib/types\";\n import { TableHeader, ProcessRow } from \"$lib/"
},
{
"path": "src/lib/components/process/TableHeader.svelte",
"chars": 1646,
"preview": "<script lang=\"ts\">\n import type { Process, Column } from \"$lib/types\";\n\n export let columns: Column[];\n export let so"
},
{
"path": "src/lib/components/process/index.ts",
"chars": 319,
"preview": "export { default as ProcessTable } from \"./ProcessTable.svelte\";\nexport { default as ProcessRow } from \"./ProcessRow.sve"
},
{
"path": "src/lib/components/stats/CpuPanel.svelte",
"chars": 1285,
"preview": "<script lang=\"ts\">\n import { faMicrochip } from \"@fortawesome/free-solid-svg-icons\";\n import { PanelHeader, ProgressBa"
},
{
"path": "src/lib/components/stats/MemoryPanel.svelte",
"chars": 1360,
"preview": "<script lang=\"ts\">\n import { faMemory } from \"@fortawesome/free-solid-svg-icons\";\n import { PanelHeader, ProgressBar, "
},
{
"path": "src/lib/components/stats/NetworkPanel.svelte",
"chars": 846,
"preview": "<script lang=\"ts\">\n import { faNetworkWired } from \"@fortawesome/free-solid-svg-icons\";\n import { PanelHeader, StatIte"
},
{
"path": "src/lib/components/stats/PanelHeader.svelte",
"chars": 997,
"preview": "<script lang=\"ts\">\n import Fa from \"svelte-fa\";\n import type { IconDefinition } from \"@fortawesome/free-solid-svg-icon"
},
{
"path": "src/lib/components/stats/ProgressBar.svelte",
"chars": 1603,
"preview": "<script lang=\"ts\">\n export let label: string;\n export let value: number;\n export let labelWidth = \"2.5rem\";\n export "
},
{
"path": "src/lib/components/stats/StatItem.svelte",
"chars": 503,
"preview": "<script lang=\"ts\">\n export let label: string;\n export let value: string;\n</script>\n\n<div class=\"stat-item\">\n <span>{l"
},
{
"path": "src/lib/components/stats/StatPanel.svelte",
"chars": 586,
"preview": "<script lang=\"ts\">\n export let title: string;\n export let flex = 1;\n</script>\n\n<div class=\"stat-panel\" style=\"--flex: "
},
{
"path": "src/lib/components/stats/StatsBar.svelte",
"chars": 1152,
"preview": "<script lang=\"ts\">\n import type { SystemStats } from \"$lib/types\";\n import {\n CpuPanel,\n MemoryPanel,\n Storag"
},
{
"path": "src/lib/components/stats/StoragePanel.svelte",
"chars": 1075,
"preview": "<script lang=\"ts\">\n import { faHardDrive } from \"@fortawesome/free-solid-svg-icons\";\n import { PanelHeader, StatItem }"
},
{
"path": "src/lib/components/stats/SystemPanel.svelte",
"chars": 949,
"preview": "<script lang=\"ts\">\n import { faServer } from \"@fortawesome/free-solid-svg-icons\";\n import { PanelHeader, StatItem } fr"
},
{
"path": "src/lib/components/stats/index.ts",
"chars": 553,
"preview": "export { default as StatsBar } from \"./StatsBar.svelte\";\nexport { default as CpuPanel } from \"./CpuPanel.svelte\";\nexport"
},
{
"path": "src/lib/components/toolbar/ColumnToggle.svelte",
"chars": 8502,
"preview": "<script lang=\"ts\">\n import Fa from \"svelte-fa\";\n import {\n faChevronDown,\n faChevronRight,\n faChevronLeft,\n "
},
{
"path": "src/lib/components/toolbar/FilterToggle.svelte",
"chars": 14023,
"preview": "<script lang=\"ts\">\n import Fa from \"svelte-fa\";\n import { faFilter } from \"@fortawesome/free-solid-svg-icons\";\n impor"
},
{
"path": "src/lib/components/toolbar/PaginationControls.svelte",
"chars": 7739,
"preview": "<script lang=\"ts\">\n import type { AppConfig } from \"$lib/types\";\n import { settingsStore } from \"$lib/stores/index\";\n "
},
{
"path": "src/lib/components/toolbar/RefreshControls.svelte",
"chars": 7212,
"preview": "<script lang=\"ts\">\n import Fa from \"svelte-fa\";\n import { faPlay, faPause } from \"@fortawesome/free-solid-svg-icons\";\n"
},
{
"path": "src/lib/components/toolbar/SearchBox.svelte",
"chars": 11533,
"preview": "<script lang=\"ts\">\n import { overlayStore } from \"$lib/stores/overlay\";\n import { onDestroy, onMount } from \"svelte\";\n"
},
{
"path": "src/lib/components/toolbar/StatusFilter.svelte",
"chars": 6007,
"preview": "<script lang=\"ts\">\n import { STATUS_OPTIONS } from \"$lib/constants\";\n import type { AppConfig } from \"$lib/types\";\n i"
},
{
"path": "src/lib/components/toolbar/ToolBar.svelte",
"chars": 2783,
"preview": "<script lang=\"ts\">\n import {\n AppInfo,\n SearchBox,\n RefreshControls,\n PaginationControls,\n ColumnToggle,"
},
{
"path": "src/lib/components/toolbar/index.ts",
"chars": 457,
"preview": "export { default as ToolBar } from \"./ToolBar.svelte\";\nexport { default as SearchBox } from \"./SearchBox.svelte\";\nexport"
},
{
"path": "src/lib/constants/index.ts",
"chars": 1779,
"preview": "export const ASCII_ART = `\n███╗ ██╗███████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ \n████╗ ██║██╔════╝██╔═══██╗██║ "
},
{
"path": "src/lib/definitions/columns.ts",
"chars": 1901,
"preview": "import type { Column } from \"$lib/types\";\nimport { formatMemorySize } from \"$lib/utils\";\n\nexport let column_definitions:"
},
{
"path": "src/lib/definitions/index.ts",
"chars": 81,
"preview": "export * from \"./columns\";\nexport * from \"./settings\";\nexport * from \"./themes\";\n"
},
{
"path": "src/lib/definitions/settings.ts",
"chars": 568,
"preview": "import type { AppConfig } from \"$lib/types\";\n\nexport const DEFAULT_CONFIG: AppConfig = {\n appearance: {\n columnVisib"
},
{
"path": "src/lib/definitions/themes.ts",
"chars": 18638,
"preview": "import type { Theme } from \"$lib/types\";\n\nexport const themes: Record<string, Theme> = {\n catppuccin: {\n name: \"catp"
},
{
"path": "src/lib/stores/index.ts",
"chars": 109,
"preview": "export * from \"./processes\";\nexport * from \"./theme\";\nexport * from \"./settings\";\nexport * from \"./overlay\";\n"
},
{
"path": "src/lib/stores/overlay.ts",
"chars": 655,
"preview": "import { writable } from \"svelte/store\";\n\ntype OverlayType =\n | \"pagination\"\n | \"refresh\"\n | \"columns\"\n | \"theme\"\n "
},
{
"path": "src/lib/stores/processes.ts",
"chars": 5289,
"preview": "import { writable, derived } from \"svelte/store\";\nimport type { Process, SystemStats } from \"$lib/types\";\nimport { invok"
},
{
"path": "src/lib/stores/settings.ts",
"chars": 1065,
"preview": "import { writable } from \"svelte/store\";\nimport type { AppConfig } from \"$lib/types\";\nimport { DEFAULT_CONFIG } from \"$l"
},
{
"path": "src/lib/stores/theme.ts",
"chars": 1732,
"preview": "import { writable } from \"svelte/store\";\nimport { themes } from \"$lib/definitions/themes\";\nimport type { Theme } from \"$"
},
{
"path": "src/lib/types/index.ts",
"chars": 2219,
"preview": "// Create a new types file to centralize interfaces\nexport interface Process {\n pid: number;\n ppid: number;\n name: st"
},
{
"path": "src/lib/utils/index.ts",
"chars": 7250,
"preview": "import type { Process } from \"$lib/types\";\nimport type { SortConfig } from \"$lib/types\";\n\nexport interface ProcessStatus"
},
{
"path": "src/routes/+layout.js",
"chars": 242,
"preview": "// Tauri doesn't have a Node.js server to do proper SSR\n// so we will use adapter-static to prerender the app (SSG)\n// S"
},
{
"path": "src/routes/+layout.svelte",
"chars": 52,
"preview": "<script>\n import \"../app.css\";\n</script>\n\n<slot />\n"
},
{
"path": "src/routes/+page.svelte",
"chars": 7285,
"preview": "<script lang=\"ts\">\n import { onMount, onDestroy } from \"svelte\";\n import { debounce } from \"$lib/utils\";\n import {\n "
},
{
"path": "src-tauri/.cargo/config.toml",
"chars": 224,
"preview": "[target.x86_64-apple-darwin]\nrustflags = [\n \"-C\", \"link-arg=-undefined\",\n \"-C\", \"link-arg=dynamic_lookup\",\n]\n\n[target."
},
{
"path": "src-tauri/.gitignore",
"chars": 166,
"preview": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Generated by Tauri\n# will have schema files "
},
{
"path": "src-tauri/Cargo.toml",
"chars": 1025,
"preview": "[package]\nname = \"neohtop\"\nversion = \"1.2.0\"\ndescription = \"A cross-platform system monitor\"\nauthors = [\"you\"]\nedition ="
},
{
"path": "src-tauri/build.rs",
"chars": 39,
"preview": "fn main() {\n tauri_build::build()\n}\n"
},
{
"path": "src-tauri/capabilities/default.json",
"chars": 395,
"preview": "{\n \"$schema\": \"../gen/schemas/desktop-schema.json\",\n \"identifier\": \"default\",\n \"description\": \"Capability for the mai"
},
{
"path": "src-tauri/src/commands.rs",
"chars": 2155,
"preview": "//! Tauri command handlers\n//!\n//! This module contains the command handlers that are exposed to the frontend\n//! throug"
},
{
"path": "src-tauri/src/main.rs",
"chars": 1352,
"preview": "#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n//! NeoHtop - A modern system monitor built with Taur"
},
{
"path": "src-tauri/src/monitoring/mod.rs",
"chars": 432,
"preview": "//! System monitoring functionality\n//!\n//! This module provides types and functionality for monitoring system resources"
},
{
"path": "src-tauri/src/monitoring/process_monitor.rs",
"chars": 6189,
"preview": "//! Process monitoring functionality\n//!\n//! This module handles monitoring and managing system processes, including\n//!"
},
{
"path": "src-tauri/src/monitoring/system_monitor.rs",
"chars": 4902,
"preview": "//! System statistics monitoring\n//!\n//! This module handles collection and monitoring of system-wide statistics\n//! inc"
},
{
"path": "src-tauri/src/monitoring/types.rs",
"chars": 3678,
"preview": "use serde::Serialize;\nuse std::fmt::Debug;\nuse sysinfo::{DiskUsage, ProcessStatus};\n\n/// Internal representation of proc"
},
{
"path": "src-tauri/src/state.rs",
"chars": 1520,
"preview": "//! Application state management\n//!\n//! This module handles the global application state, including system monitoring\n/"
},
{
"path": "src-tauri/src/ui/mod.rs",
"chars": 213,
"preview": "//! User interface functionality\n//!\n//! This module handles UI-specific functionality, including window effects\n//! and"
},
{
"path": "src-tauri/src/ui/window.rs",
"chars": 1112,
"preview": "//! Window effects and customization\n//!\n//! Provides platform-specific window effects like transparency and vibrancy.\n\n"
},
{
"path": "src-tauri/tauri.conf.json",
"chars": 1172,
"preview": "{\n \"$schema\": \"../node_modules/@tauri-apps/cli/config.schema.json\",\n \"build\": {\n \"beforeBuildCommand\": \"npm run bui"
},
{
"path": "svelte.config.js",
"chars": 497,
"preview": "// Tauri doesn't have a Node.js server to do proper SSR\n// so we will use adapter-static to prerender the app (SSG)\n// S"
},
{
"path": "vite.config.js",
"chars": 836,
"preview": "import { defineConfig } from \"vite\";\nimport { sveltekit } from \"@sveltejs/kit/vite\";\n\nconst host = process.env.TAURI_DEV"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the Abdenasser/neohtop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 95 files (276.9 KB), approximately 79.4k tokens, and a symbol index with 72 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.