diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ca5fff5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to Sysora + +First off, thank you for considering contributing to Sysora! It's people like you that make Sysora such a great tool for everyone. + +## Code of Conduct + +By participating in this project, you agree to abide by our terms. We expect all contributors to be respectful and collaborative. + +## How Can I Contribute? + +### Reporting Bugs +* Check the existing issues to see if the bug has already been reported. +* If not, open a new issue. Include a clear title, a description of the problem, and steps to reproduce the issue. +* Attach screenshots or logs if possible. + +### Suggesting Enhancements +* Open a new issue with the tag `enhancement`. +* Explain why the feature would be useful and how it should work. + +### Your First Code Contribution +1. **Fork the repo**: Click the "Fork" button at the top right of this page. +2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR_USERNAME/sysora.git + cd sysora + ``` +3. **Install dependencies**: + ```bash + npm install + ``` +4. **Create a branch**: + ```bash + git checkout -b feat/your-feature-name + ``` +5. **Make your changes**: Ensure your code follows the existing style. +6. **Test your changes**: Run `npm run tauri dev` to verify. +7. **Commit your changes**: + ```bash + git commit -m "feat: add some cool feature" + ``` +8. **Push to your fork**: + ```bash + git push origin feat/your-feature-name + ``` +9. **Open a Pull Request**: Submit your PR against the `main` branch of the original repo. + +## Tech Stack Knowledge +To contribute effectively, you should be familiar with: +* **Rust**: Used for the backend logic and OS integrations. +* **Tauri v2**: The bridge between Rust and the web frontend. +* **React**: Used for building the dashboard UI. +* **Tailwind CSS**: Used for all styling and the dark/light theme system. +* **SQLite (rusqlite)**: Used for historical data persistence. + +## Development Commands + +```bash +# Run in dev mode (with HMR) +npm run tauri dev + +# Build production bundles +npm run tauri build +``` + +--- + +*Happy hacking!* πŸš€ diff --git a/README.md b/README.md index 06e6841..f52fc21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sysora β€” System Monitor & Manager -> Cross-platform desktop app to monitor memory, kill hungry processes, inspect system specs, scan disk usage, and check battery health β€” all from a modern dark dashboard with a tray icon. +> Cross-platform desktop app to monitor memory, kill hungry processes, inspect system specs, scan disk usage, track fans, and view historical usage trends β€” all from a modern dark/light dashboard or your terminal. Built with **Rust + Tauri v2** on the backend and **React 19 + Vite + Tailwind CSS** on the frontend. Runs natively on **Ubuntu**, **macOS**, and **Windows**. @@ -8,7 +8,7 @@ Built with **Rust + Tauri v2** on the backend and **React 19 + Vite + Tailwind C ## πŸ“₯ Download Latest Release -Get the latest stable version (Phase 2) for your operating system: +Get the latest stable version (Phase 3) for your operating system: | Platform | Installer | Portability | |---|---|---| @@ -18,23 +18,32 @@ Get the latest stable version (Phase 2) for your operating system: --- -## πŸš€ Features (Phase 2 Stable) +## πŸš€ Features (Phase 3 Stable) ### πŸ“Š Real-time Monitoring - **Resource Pulse**: Live CPU and RAM usage history sparklines (last 60s). - **Process Manager**: Kill hungry processes with one click, search by name, and sort by memory usage. - **Stat Cards**: Instant glance at RAM, CPU, Disk, and Battery health/status. +- **Fan Monitoring**: Track fan speeds (RPM) and system temperatures in real-time (Linux). + +### πŸ“ˆ Observability & History +- **SQLite History**: Periodic system snapshots stored in a local database. +- **Trend Charts**: Visualize CPU, RAM, and Disk usage over the last 1h, 6h, 24h, or 7 days. +- **Usage Peaks**: Automatically calculate average and peak resource usage for any time range. ### πŸ’Ύ Storage & Files - **Disk usage**: Monitor all mounted partitions and removable drives. - **Deep Scanner**: Find what's eating your space! Scan any directory to find the largest files and folders. - **Safe Purge**: Delete heavy files/folders directly from the scanner with confirmation. +### ⌨️ CLI Interface +- **Headless Mode**: Run `sysora status` for a quick system snapshot in your terminal. +- **Tooling**: Export JSON data for scripts or check specific sensors with `sysora fans`. + ### βš™οΈ App Management & Settings +- **Theme Toggles**: Seamlessly switch between **Dark**, **Light**, and **System** themes. - **Installed Apps**: List and manage installed applications. -- **Persistence**: Save your preferences for refresh rates and alert thresholds. -- **Tray Power**: Complete control from the system tray: quick specs, settings, and one-click toggle. -- **Native Experience**: "Close to Tray" and "Start minimized" support. +- **Tray Power**: Complete control from the system tray: quick specs, settings, and history access. --- @@ -43,20 +52,20 @@ Get the latest stable version (Phase 2) for your operating system: ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ React 19 + Vite frontend β”‚ -β”‚ Memory β”‚ Processes β”‚ Apps β”‚ Disk β”‚ Sys Info β”‚ +β”‚ Memory β”‚ Processes β”‚ Network β”‚ History β”‚ Sys Info β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ invoke() / emit() (Tauri IPC) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Tauri v2 bridge β”‚ -β”‚ Commands: scan_directory Β· kill_process Β· settings β”‚ +β”‚ Commands: get_history Β· get_fans Β· save_settings β”‚ β”‚ Events: scan-progress Β· process-update β”‚ -β”‚ Tray: toggle Β· system-info Β· settings Β· quit β”‚ -└──────────────────┬───────────────────────────────────┐ +β”‚ Tray: toggle Β· status Β· settings Β· quit β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Rust backend β”‚ -β”‚ sysinfo crate β”‚ WalkDir scanner β”‚ Battery readerβ”‚ -β”‚ /proc Β· WMI Β· sysctl (cross-platform OS APIs) β”‚ +β”‚ SQLite (rusqlite) β”‚ sysinfo crate β”‚ hwmon reader β”‚ +β”‚ Background Task β”‚ Battery reader β”‚ CLI (clap) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Runs on Ubuntu Β· macOS Β· Windows ``` @@ -68,6 +77,8 @@ Get the latest stable version (Phase 2) for your operating system: | Layer | Technology | |---|---| | **Backend** | Rust, Tauri v2 | +| **Database** | SQLite (rusqlite) | +| **CLI** | Clap v4 | | **Frontend** | React 19, Vite, Tailwind CSS | | **State** | Zustand, TanStack Query | | **Charts** | Recharts | @@ -81,11 +92,13 @@ Get the latest stable version (Phase 2) for your operating system: sysora/ β”œβ”€β”€ src/ # React frontend β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ β”œβ”€β”€ charts/ # History graphs -β”‚ β”‚ β”œβ”€β”€ tabs/ # Memory, Apps, Disk, Settings -β”‚ β”‚ └── layout/ # Shell, Sidebar, StatCards +β”‚ β”‚ β”œβ”€β”€ charts/ # History & Network graphs +β”‚ β”‚ β”œβ”€β”€ tabs/ # Memory, History, Disk, Settings +β”‚ β”‚ └── layout/ # Shell, Sidebar, TopBar β”œβ”€β”€ src-tauri/ # Rust backend β”‚ β”œβ”€β”€ src/lib.rs # Core logic & Commands +β”‚ β”œβ”€β”€ src/db.rs # SQLite persistence +β”‚ β”œβ”€β”€ src/cli.rs # CLI command handling β”‚ └── capabilities/ # Security & Permissions β”œβ”€β”€ images/ # Project assets (Logo, Icons) └── .github/workflows/ # CI/CD Release pipeline @@ -99,11 +112,7 @@ sysora/ - **Node.js** β‰₯ 20 - **Rust** (stable) - **Tauri v2 CLI** (`npm install -g @tauri-apps/cli`) -- **Linux (Ubuntu/Debian) dependencies**: - ```bash - sudo apt update - sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev - ``` +- **Linux dependencies**: `libwebkit2gtk-4.1-dev`, `libayatana-appindicator3-dev`, etc. ### Run in development @@ -125,46 +134,31 @@ npm run tauri dev ### βœ… Phase 1 β€” Foundation - [x] Live process list + Kill button -- [x] Disk usage bars - [x] System specs + Battery health -- [x] Tray popup snapshot -### βœ… Phase 2 β€” Management (Current) +### βœ… Phase 2 β€” Management - [x] **App Manager**: List installed applications. -- [x] **Settings**: Refresh rate, startup behavior, and persistence. -- [x] **Disk Scanner**: Find largest files/folders with deletion support. -- [x] **Resource History**: 60s CPU/RAM chart in Memory tab. -- [x] **Persistence**: Save user settings to JSON. - -### πŸ”² Phase 3 β€” Polish -- [ ] Notifications: Alert when RAM/CPU crosses threshold. -- [ ] Branded icon + Splash screen. -- [ ] Export system report as PDF. -- [ ] Full Windows uninstallation logic. - ---- +- [x] **Settings**: Refresh rate, startup behavior. +- [x] **Disk Scanner**: Find largest files/folders. -## Battery Health β€” How It Works +### βœ… Phase 3 β€” Polish & Observability (Current) +- [x] **Dark/Light Themes**: Full Tailwind support. +- [x] **CLI Snapshot**: `sysora status` command. +- [x] **SQLite Tracking**: 30-day historical trends. +- [x] **Cooling & Fans**: Real-time RPM tracking. -Sysora distinguishes between two different battery numbers that most tools confuse: - -| Metric | What it means | -|---|---| -| **Current charge** | How full the battery is right now (0–100%) | -| **Battery health** | Current max capacity vs the original design capacity | +### πŸ”² Phase 4 β€” Enterprise +- [ ] Export system report as PDF. +- [ ] Network traffic breakdown per process. +- [ ] Custom alert rules (Email/Slack notifications). -A battery with **60% health** that is fully charged will only last 60% as long as it did when it was new β€” even though it shows "100% charge". +--- -This is read from `/sys/class/power_supply/BAT*/` on Linux (`energy_full` vs `energy_full_design`). macOS and Windows native APIs are supported via the `battery` crate. ---- ## Contributing -1. Fork the repo -2. Create a feature branch: `git checkout -b feat/my-feature` -3. Commit changes: `git commit -m "feat: add feature"` -4. Open a PR against `main` +We love contributions! Please see our [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to get started. --- diff --git a/package-lock.json b/package-lock.json index ed4164c..de92006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "sysora", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sysora", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { "@tanstack/react-query": "^5.56.2", "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-autostart": "^2.5.1", + "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-shell": "^2.0.0", "lucide-react": "^0.441.0", @@ -1935,6 +1936,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tauri-apps/plugin-notification": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz", diff --git a/package.json b/package.json index d18ccaf..b7d2980 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sysora", "private": true, - "version": "0.2.0", + "version": "0.3.0", "type": "module", "scripts": { "dev": "vite", @@ -13,6 +13,7 @@ "@tanstack/react-query": "^5.56.2", "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-autostart": "^2.5.1", + "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-shell": "^2.0.0", "lucide-react": "^0.441.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5d55532..1c3d910 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -17,6 +29,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -41,12 +71,94 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -224,6 +336,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "base64" version = "0.21.7" @@ -268,6 +423,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -283,6 +444,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -335,6 +505,23 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" @@ -442,6 +629,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -485,11 +674,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -627,6 +880,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -977,6 +1236,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1025,12 +1304,45 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1349,6 +1661,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1497,12 +1819,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1518,6 +1860,15 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -1791,11 +2142,38 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "moxcms", "num-traits", "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + [[package]] name = "indexmap" version = "1.9.3" @@ -1828,6 +2206,17 @@ dependencies = [ "cfb", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1853,6 +2242,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1926,6 +2330,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.98" @@ -1971,6 +2385,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lazycell" version = "1.3.0" @@ -1983,6 +2403,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2022,6 +2448,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2041,6 +2477,23 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2068,6 +2521,32 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lopdf" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" +dependencies = [ + "encoding_rs", + "flate2", + "itoa", + "linked-hash-map", + "log", + "md5", + "pom", + "time", + "weezl", +] + [[package]] name = "mac-notification-sys" version = "0.6.12" @@ -2100,6 +2579,22 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.8.0" @@ -2215,6 +2710,30 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "notify-rust" version = "4.17.0" @@ -2238,12 +2757,53 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2475,7 +3035,13 @@ dependencies = [ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" @@ -2515,6 +3081,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser", +] + [[package]] name = "pango" version = "0.18.3" @@ -2569,6 +3144,18 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2710,6 +3297,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pom" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" +dependencies = [ + "bstr", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2750,6 +3346,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "printpdf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d" +dependencies = [ + "js-sys", + "lopdf", + "owned_ttf_parser", + "time", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2812,12 +3420,46 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "pxfm" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -2886,6 +3528,56 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3026,6 +3718,50 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3377,6 +4113,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "1.0.3" @@ -3557,21 +4302,28 @@ dependencies = [ [[package]] name = "sysora" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64 0.22.1", "battery", + "chrono", + "clap", + "colored", "plist", + "printpdf", + "rusqlite", "serde", "serde_json", "sysinfo", "tauri", "tauri-build", "tauri-plugin-autostart", + "tauri-plugin-dialog", "tauri-plugin-notification", "tauri-plugin-shell", "tokio", "walkdir", + "windows-icons", "winreg 0.52.0", ] @@ -3789,6 +4541,48 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-notification" version = "2.3.3" @@ -4004,6 +4798,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -4310,6 +5118,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" + [[package]] name = "typeid" version = "1.0.3" @@ -4439,6 +5253,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -4451,6 +5271,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4730,6 +5567,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -4786,6 +5629,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -4820,6 +5673,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4857,6 +5723,18 @@ dependencies = [ "windows-threading", ] +[[package]] +name = "windows-icons" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd18e61837c404baa46146d3ea12241073ee7b4457d5ad73751d0e8f49dad9a" +dependencies = [ + "base64 0.22.1", + "image", + "winapi", + "windows 0.58.0", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -4868,6 +5746,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4890,6 +5779,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -4932,6 +5832,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4950,6 +5859,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -5492,6 +6411,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.2" @@ -5656,6 +6581,30 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.11.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b5a252b..3bfeac7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sysora" -version = "0.2.0" +version = "0.3.0" description = "Sysora β€” cross-platform system monitor & manager" authors = ["chojuninengu"] edition = "2021" @@ -19,17 +19,24 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = ["tray-icon", "image-ico", "image-png"] } tauri-plugin-shell = "2" +tauri-plugin-dialog = "2" tauri-plugin-autostart = "2" tauri-plugin-notification = "2" +chrono = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" sysinfo = "0.33" +rusqlite = { version = "0.31", features = ["bundled"] } tokio = { version = "1", features = ["full"] } +printpdf = "0.7" walkdir = "2.5.0" base64 = "0.22" battery = "0.7.0" +clap = { version = "4", features = ["derive"] } +colored = "2" [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.52" +windows-icons = "0.1.0" [target.'cfg(target_os = "macos")'.dependencies] plist = "1.7" diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index b22c303..78a6fbe 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -19,6 +19,8 @@ "notification:allow-request-permission", "autostart:allow-enable", "autostart:allow-disable", - "autostart:allow-is-enabled" + "autostart:allow-is-enabled", + "dialog:allow-save", + "dialog:default" ] } diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs new file mode 100644 index 0000000..700f331 --- /dev/null +++ b/src-tauri/src/cli.rs @@ -0,0 +1,315 @@ +use clap::{Parser, Subcommand}; +use colored::*; +use sysinfo::{System, Components, Disks}; +use sysora_lib::*; +use serde_json; + +#[derive(Parser)] +#[command(name = "sysora")] +#[command(version = "0.3.0")] +#[command(about = "Sysora CLI β€” System monitoring from your terminal", long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Option, + + /// Output in JSON format + #[arg(short, long, global = true)] + pub json: bool, + + /// Hidden arg for compatibility with GUI autostart + #[arg(long, hide = true)] + pub minimized: bool, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Show a quick system snapshot (default) + Status, + /// List top processes by RAM usage + Processes { + /// Number of processes to show + #[arg(short, long, default_value_t = 10)] + top: usize, + }, + /// Show detailed disk usage per mount + Disk, + /// Show battery health and capacity details + Battery, + /// Show system fan speeds + Fans, +} + +pub fn run_cli(cli: Cli) { + match cli.command.unwrap_or(Commands::Status) { + Commands::Status => show_status(cli.json), + Commands::Processes { top } => show_processes(top, cli.json), + Commands::Disk => show_disk(cli.json), + Commands::Battery => show_battery(cli.json), + Commands::Fans => show_fans(cli.json), + } +} + +fn bar(pct: f32, width: usize) -> String { + let filled = (pct / 100.0 * width as f32).round() as usize; + let filled = filled.min(width); + let mut s = String::new(); + for i in 0..width { + if i < filled { + s.push('β–ˆ'); + } else { + s.push('β–‘'); + } + } + s +} + +fn show_status(json: bool) { + let mut sys = System::new_all(); + sys.refresh_all(); + + let cpu_count = sys.cpus().len(); + let cpu_usage = if cpu_count > 0 { + sys.cpus().iter().map(|c| c.cpu_usage()).sum::() / cpu_count as f32 + } else { 0.0 }; + + let total_mem = sys.total_memory(); + let used_mem = sys.used_memory(); + let mem_pct = if total_mem > 0 { (used_mem as f32 / total_mem as f32) * 100.0 } else { 0.0 }; + + let mut comps = Components::new_with_refreshed_list(); + comps.refresh(true); + let max_temp = comps.iter().filter_map(|c| c.temperature()).fold(0.0, f32::max); + + let mut disks = Disks::new_with_refreshed_list(); + disks.refresh(true); + let mut disk_used = 0; + let mut disk_total = 0; + for d in disks.iter() { + disk_total += d.total_space(); + disk_used += d.total_space() - d.available_space(); + } + let disk_pct = if disk_total > 0 { (disk_used as f64 / disk_total as f64) * 100.0 } else { 0.0 }; + + let bat = read_battery(); + let fans = read_fans(); + + if json { + let output = serde_json::json!({ + "os": format!("{} {}", System::name().unwrap_or_default(), System::os_version().unwrap_or_default()), + "hostname": System::host_name().unwrap_or_default(), + "cpu": { "usage_pct": cpu_usage, "cores": cpu_count, "brand": sys.cpus().first().map(|c| c.brand()).unwrap_or_default() }, + "memory": { "total_bytes": total_mem, "used_bytes": used_mem, "usage_pct": mem_pct }, + "disk": { "total_bytes": disk_total, "used_bytes": disk_used, "usage_pct": disk_pct }, + "temp": { "max_celsius": max_temp }, + "battery": bat, + "fans": fans + }); + println!("{}", serde_json::to_string_pretty(&output).unwrap()); + return; + } + + println!("{} {} Β· {} {} Β· hostname: {}", + "Sysora".brand_color(), + "v0.3.0".dimmed(), + System::name().unwrap_or_default().cyan(), + System::os_version().unwrap_or_default().cyan(), + System::host_name().unwrap_or_default().yellow() + ); + + // RAM + let ram_bar = bar(mem_pct, 10); + let ram_color = if mem_pct > 85.0 { Color::Red } else if mem_pct > 70.0 { Color::Yellow } else { Color::Green }; + println!("{:<8} {} {} / {} ({:.0}%)", + "RAM".bold(), + ram_bar.color(ram_color), + fmt_bytes(used_mem), + fmt_bytes(total_mem), + mem_pct + ); + + // CPU + let cpu_bar = bar(cpu_usage, 10); + let cpu_color = if cpu_usage > 80.0 { Color::Red } else if cpu_usage > 60.0 { Color::Yellow } else { Color::Green }; + println!("{:<8} {} {:.0}% Β· {} cores Β· {}", + "CPU".bold(), + cpu_bar.color(cpu_color), + cpu_usage, + cpu_count, + sys.cpus().first().map(|c| c.brand()).unwrap_or_default().dimmed() + ); + + // Disk + let d_bar = bar(disk_pct as f32, 10); + let d_color = if disk_pct > 90.0 { Color::Red } else if disk_pct > 75.0 { Color::Yellow } else { Color::Green }; + let d_warn = if disk_pct > 95.0 { " ⚠".red().bold().to_string() } else { "".to_string() }; + println!("{:<8} {} {} / {} ({:.0}%){}", + "Disk".bold(), + d_bar.color(d_color), + fmt_bytes(disk_used), + fmt_bytes(disk_total), + disk_pct, + d_warn + ); + + // Battery + if bat.present { + let b_bar = bar(bat.charge_percent as f32, 10); + let b_color = if bat.charge_percent < 20.0 { Color::Red } else if bat.charge_percent < 40.0 { Color::Yellow } else { Color::Green }; + println!("{:<8} {} {:.0}% Β· Health {:.0}% Β· {} cycles", + "Battery".bold(), + b_bar.color(b_color), + bat.charge_percent, + bat.health_percent, + bat.cycle_count.unwrap_or(0) + ); + } + + // Temp + let t_bar = bar((max_temp / 100.0 * 100.0).min(100.0), 10); + let t_color = if max_temp > 80.0 { Color::Red } else if max_temp > 60.0 { Color::Yellow } else { Color::Green }; + println!("{:<8} {} {:.0}Β°C Β· Max recorded", + "Temp".bold(), + t_bar.color(t_color), + max_temp + ); + + // Fans + if !fans.is_empty() { + let f_label = if fans.len() == 1 { "Fan" } else { "Fans" }; + let avg_rpm = fans.iter().map(|f| f.rpm).sum::() / fans.len() as u32; + println!("{:<8} {} sensors detected Β· avg {} RPM", + f_label.bold(), + fans.len(), + avg_rpm + ); + } +} + +fn show_processes(top_n: usize, json: bool) { + let mut sys = System::new_all(); + sys.refresh_all(); + + let mut procs: Vec<_> = sys.processes().values().collect(); + procs.sort_by(|a, b| b.memory().cmp(&a.memory())); + + if json { + let output: Vec<_> = procs.iter().take(top_n).map(|p| { + serde_json::json!({ + "pid": p.pid().as_u32(), + "name": p.name(), + "memory_bytes": p.memory(), + "cpu_usage": p.cpu_usage() + }) + }).collect(); + println!("{}", serde_json::to_string_pretty(&output).unwrap()); + return; + } + + println!("{:<8} {:<25} {:<15} {:<10}", "PID".bold(), "NAME".bold(), "MEMORY".bold(), "CPU".bold()); + println!("{}", "─".repeat(60).dimmed()); + for p in procs.iter().take(top_n) { + println!("{:<8} {:<25} {:<15} {:<10.1}%", + p.pid().as_u32(), + p.name().to_string_lossy().truncate(24), + fmt_bytes(p.memory()), + p.cpu_usage() + ); + } +} + +fn show_disk(json: bool) { + let mut disks = Disks::new_with_refreshed_list(); + disks.refresh(true); + + if json { + println!("{}", serde_json::to_string_pretty(&disks.iter().map(|d| { + serde_json::json!({ + "name": d.name().to_string_lossy(), + "mount": d.mount_point().to_string_lossy(), + "total_bytes": d.total_space(), + "available_bytes": d.available_space() + }) + }).collect::>()).unwrap()); + return; + } + + println!("{:<15} {:<20} {:<15} {:<10}", "NAME".bold(), "MOUNT".bold(), "USAGE".bold(), "FREE".bold()); + println!("{}", "─".repeat(65).dimmed()); + for d in disks.iter() { + let used = d.total_space() - d.available_space(); + let pct = if d.total_space() > 0 { (used as f64 / d.total_space() as f64) * 100.0 } else { 0.0 }; + println!("{:<15} {:<20} {:<15} {:<10}", + d.name().to_string_lossy().truncate(14), + d.mount_point().to_string_lossy().truncate(19), + format!("{} ({:.0}%)", fmt_bytes(used), pct), + fmt_bytes(d.available_space()) + ); + } +} + +fn show_battery(json: bool) { + let bat = read_battery(); + if json { + println!("{}", serde_json::to_string_pretty(&bat).unwrap()); + return; + } + + if !bat.present { + println!("{}", "No battery detected.".yellow()); + return; + } + + println!("{}", "Battery Health Details".bold().cyan()); + println!("{:<20} {:.0}%", "Charge:", bat.charge_percent); + println!("{:<20} {:.0}% ({})", "Health:", bat.health_percent, health_label(bat.health_percent)); + println!("{:<20} {}", "Cycles:", bat.cycle_count.unwrap_or(0)); + println!("{:<20} {}", "Status:", bat.status); + if let Some(m) = bat.design_capacity_mwh { println!("{:<20} {} mWh", "Design Cap:", m); } + if let Some(m) = bat.current_capacity_mwh { println!("{:<20} {} mWh", "Current Cap:", m); } +} + +fn show_fans(json: bool) { + let fans = read_fans(); + if json { + println!("{}", serde_json::to_string_pretty(&fans).unwrap()); + return; + } + + if fans.is_empty() { + println!("{}", "No fan sensors detected.".yellow()); + return; + } + + println!("{}", "System Cooling & Fans".bold().cyan()); + println!("{:<25} {:<10}", "LABEL".bold(), "SPEED".bold()); + println!("{}", "─".repeat(40).dimmed()); + for f in fans { + println!("{:<25} {} RPM", f.label.truncate(24), f.rpm); + } +} + +trait BrandColor { + fn brand_color(&self) -> String; +} +impl BrandColor for str { + fn brand_color(&self) -> String { self.color(Color::TrueColor { r: 99, g: 102, b: 241 }).bold().to_string() } +} + +trait StringExt { + fn truncate(&self, len: usize) -> String; +} +impl StringExt for String { + fn truncate(&self, len: usize) -> String { + if self.len() > len { format!("{}…", &self[..len-1]) } else { self.clone() } + } +} +impl StringExt for &str { + fn truncate(&self, len: usize) -> String { + if self.len() > len { format!("{}…", &self[..len-1]) } else { self.to_string() } + } +} +impl StringExt for std::borrow::Cow<'_, str> { + fn truncate(&self, len: usize) -> String { + if self.len() > len { format!("{}…", &self[..len-1]) } else { self.to_string() } + } +} diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs new file mode 100644 index 0000000..74770f6 --- /dev/null +++ b/src-tauri/src/db.rs @@ -0,0 +1,130 @@ +use rusqlite::{Connection, Result, params}; +use std::path::PathBuf; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone)] +pub struct HistoricalPoint { + pub ts: u64, + pub cpu_pct: f32, + pub ram_used: u64, + pub ram_total: u64, + pub disk_used: u64, + pub disk_total: u64, + pub cpu_temp: f32, +} + +pub struct DbManager { + conn: Connection, +} + +impl DbManager { + pub fn new(path: PathBuf) -> Result { + let conn = Connection::open(path)?; + + // Initialize table + conn.execute( + "CREATE TABLE IF NOT EXISTS snapshots ( + id INTEGER PRIMARY KEY, + ts INTEGER NOT NULL, + cpu_pct REAL, + ram_used INTEGER, + ram_total INTEGER, + disk_used INTEGER, + disk_total INTEGER, + cpu_temp REAL + )", + [], + )?; + + // Create index on timestamp for fast queries + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON snapshots (ts)", + [], + )?; + + Ok(Self { conn }) + } + + pub fn save_snapshot(&self, p: &HistoricalPoint) -> Result<()> { + self.conn.execute( + "INSERT INTO snapshots (ts, cpu_pct, ram_used, ram_total, disk_used, disk_total, cpu_temp) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + p.ts, + p.cpu_pct, + p.ram_used, + p.ram_total, + p.disk_used, + p.disk_total, + p.cpu_temp + ], + )?; + Ok(()) + } + + pub fn get_history(&self, hours: u32) -> Result> { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let start_ts = now.saturating_sub(hours as u64 * 3600); + + let mut stmt = self.conn.prepare( + "SELECT ts, cpu_pct, ram_used, ram_total, disk_used, disk_total, cpu_temp + FROM snapshots + WHERE ts >= ?1 + ORDER BY ts ASC" + )?; + + let rows = stmt.query_map(params![start_ts], |row| { + Ok(HistoricalPoint { + ts: row.get(0)?, + cpu_pct: row.get(1)?, + ram_used: row.get(2)?, + ram_total: row.get(3)?, + disk_used: row.get(4)?, + disk_total: row.get(5)?, + cpu_temp: row.get(6)?, + }) + })?; + + let mut points = Vec::new(); + for row in rows { + points.push(row?); + } + Ok(points) + } + + pub fn prune(&self, days: u32) -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let limit = now.saturating_sub(days as u64 * 86400); + + let count = self.conn.execute( + "DELETE FROM snapshots WHERE ts < ?1", + params![limit], + )?; + Ok(count) + } + + pub fn clear(&self) -> Result<()> { + self.conn.execute("DELETE FROM snapshots", [])?; + Ok(()) + } + + pub fn get_db_size(&self) -> Result { + // Query the size of the database file via SQLite pragmas or just check file size. + // Actually, let's use the file size from metadata if we had the path. + // Or we can just query page_count * page_size. + let page_count: i64 = self.conn.query_row("PRAGMA page_count", [], |r| r.get(0))?; + let page_size: i64 = self.conn.query_row("PRAGMA page_size", [], |r| r.get(0))?; + Ok((page_count * page_size) as u64) + } + + pub fn get_count(&self) -> Result { + let count: usize = self.conn.query_row("SELECT COUNT(*) FROM snapshots", [], |r| r.get(0))?; + Ok(count) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c07b70c..63abc28 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,9 @@ use tauri::{ }; use tokio::time::{sleep, Duration}; +mod db; +use db::{DbManager, HistoricalPoint}; + // ─── Shared system state ──────────────────────────────────────────────────── #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct AppSettings { @@ -17,6 +20,9 @@ pub struct AppSettings { pub ram_alert_threshold: f32, pub cpu_alert_threshold: f32, pub start_minimized: bool, + pub temp_threshold: f32, + pub temp_unit: String, + pub theme: String, } impl Default for AppSettings { @@ -27,6 +33,9 @@ impl Default for AppSettings { ram_alert_threshold: 85.0, cpu_alert_threshold: 80.0, start_minimized: false, + temp_threshold: 85.0, + temp_unit: "c".to_string(), + theme: "system".to_string(), } } } @@ -44,6 +53,10 @@ pub struct SysState { pub history: Mutex>, pub scan_results: Mutex>, pub is_scanning: Mutex, + pub networks: Mutex, + pub net_history: Mutex>, + pub components: Mutex, + pub db: Mutex, } // ─── Data types (serialized to JSON for the frontend) ─────────────────────── @@ -73,6 +86,8 @@ pub struct SystemSnapshot { pub kernel_version: String, pub hostname: String, pub uptime_secs: u64, + pub cpu_temp: f32, + pub system_temp: f32, } #[derive(Debug, Serialize, Clone)] @@ -98,6 +113,26 @@ pub struct BatteryInfo { pub time_to_empty_mins: Option, } +#[derive(Debug, Serialize, Clone, Default)] +pub struct NetworkInterface { + pub name: String, + pub rx_bytes: u64, + pub tx_bytes: u64, + pub rx_speed: u64, + pub tx_speed: u64, + pub total_rx: u64, + pub total_tx: u64, + pub mac_address: String, + pub ip_address: String, +} + +#[derive(Debug, Serialize, Clone)] +pub struct NetworkHistory { + pub ts: u64, + pub rx_speed: u64, + pub tx_speed: u64, +} + #[derive(Debug, Serialize, Clone)] pub struct TraySnapshot { pub cpu_usage: f32, @@ -105,6 +140,7 @@ pub struct TraySnapshot { pub total_memory: u64, pub disk_used_pct: f32, pub battery: BatteryInfo, + pub network: (u64, u64), // (total_rx_speed, total_tx_speed) } #[derive(Debug, Serialize, Clone, Default)] @@ -125,6 +161,20 @@ pub struct DiskEntry { pub is_dir: bool, } +#[derive(Debug, Serialize, Clone)] +pub struct TempReading { + pub label: String, + pub current_celsius: f32, + pub max_celsius: f32, + pub critical_celsius: Option, +} + +#[derive(Debug, Serialize, Clone)] +pub struct FanReading { + pub label: String, + pub rpm: u32, +} + #[derive(Debug, Serialize, Clone)] pub struct ScanProgress { pub scanned: u64, @@ -133,22 +183,37 @@ pub struct ScanProgress { // ─── Helpers ───────────────────────────────────────────────────────────────── -fn fmt_bytes(bytes: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - if bytes >= GB { - format!("{:.2} GB", bytes as f64 / GB as f64) - } else if bytes >= MB { - format!("{:.1} MB", bytes as f64 / MB as f64) - } else if bytes >= KB { - format!("{:.0} KB", bytes as f64 / KB as f64) +pub fn fmt_bytes(bytes: u64) -> String { + if bytes > 1024 * 1024 * 1024 { + format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) + } else if bytes > 1024 * 1024 { + format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) } else { - format!("{} B", bytes) + format!("{:.1} KB", bytes as f64 / 1024.0) } } -fn read_battery() -> BatteryInfo { +pub fn fmt_uptime(secs: u64) -> String { + let days = secs / 86400; + let hours = (secs % 86400) / 3600; + let mins = (secs % 3600) / 60; + if days > 0 { + format!("{}d {}h {}m", days, hours, mins) + } else if hours > 0 { + format!("{}h {}m", hours, mins) + } else { + format!("{}m", mins) + } +} + +pub fn health_label(pct: f32) -> &'static str { + if pct >= 90.0 { "Excellent" } + else if pct >= 80.0 { "Good" } + else if pct >= 50.0 { "Degraded" } + else { "Replace Soon" } +} + +pub fn read_battery() -> BatteryInfo { #[cfg(target_os = "linux")] { use std::fs; @@ -236,6 +301,55 @@ fn read_battery() -> BatteryInfo { } } +pub fn read_fans() -> Vec { + let mut fans = Vec::new(); + #[cfg(target_os = "linux")] + { + use std::fs; + let hwmon_path = "/sys/class/hwmon"; + if let Ok(entries) = fs::read_dir(hwmon_path) { + for entry in entries.filter_map(|e| e.ok()) { + let base = entry.path(); + let name = fs::read_to_string(base.join("name")) + .unwrap_or_default() + .trim() + .to_string(); + + // Look for fan*_input files + if let Ok(hw_entries) = fs::read_dir(&base) { + for hw_entry in hw_entries.filter_map(|e| e.ok()) { + let filename = hw_entry.file_name().to_string_lossy().to_string(); + if filename.starts_with("fan") && filename.ends_with("_input") { + let prefix = filename.trim_end_matches("_input"); + let rpm = fs::read_to_string(hw_entry.path()) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + if rpm > 0 { + // Try to get a label + let label = fs::read_to_string(base.join(format!("{}_label", prefix))) + .unwrap_or_else(|_| { + if !name.is_empty() { + format!("{} {}", name, prefix.replace("fan", "Fan ")) + } else { + prefix.replace("fan", "Fan ").to_string() + } + }) + .trim() + .to_string(); + + fans.push(FanReading { label, rpm }); + } + } + } + } + } + } + } + fans +} + fn resolve_linux_icon(icon_name: &str) -> String { if icon_name.is_empty() { return String::new(); } if icon_name.starts_with('/') { @@ -444,6 +558,22 @@ fn get_system_info(state: State) -> SystemSnapshot { kernel_version: System::kernel_version().unwrap_or_default(), hostname: System::host_name().unwrap_or_default(), uptime_secs: System::uptime(), + cpu_temp: { + let mut components = state.components.lock().unwrap(); + components.refresh(true); + components.iter() + .filter(|c| c.label().to_lowercase().contains("cpu") || c.label().to_lowercase().contains("package")) + .filter_map(|c| c.temperature()) + .fold(0.0, f32::max) + }, + system_temp: { + let components = state.components.lock().unwrap(); + components.iter() + .filter(|c| c.label().to_lowercase().contains("mb") || c.label().to_lowercase().contains("motherboard") || c.label().to_lowercase().contains("systin") || c.label().to_lowercase().contains("composite")) + .filter_map(|c| c.temperature()) + .next() + .unwrap_or(0.0) + } } } @@ -469,6 +599,250 @@ fn get_disks() -> Vec { .collect() } +/// Returns all network interfaces and their current stats. +#[tauri::command] +fn get_network_stats(state: State) -> Vec { + let mut networks = state.networks.lock().unwrap(); + networks.refresh(true); + + networks.iter().map(|(name, data)| { + NetworkInterface { + name: name.clone(), + rx_bytes: data.received(), + tx_bytes: data.transmitted(), + rx_speed: data.received(), // Since we refresh every X seconds, this is raw for now + tx_speed: data.transmitted(), + total_rx: data.total_received(), + total_tx: data.total_transmitted(), + mac_address: data.mac_address().to_string(), + ip_address: data.ip_networks().iter().map(|n| n.to_string()).collect::>().join(", "), + } + }).collect() +} + +/// Returns historical trend data for the last N hours. +#[tauri::command] +fn get_historical_trends(state: State, hours: u32) -> Result, String> { + let db = state.db.lock().unwrap(); + db.get_history(hours).map_err(|e| e.to_string()) +} + +/// Clears all historical data from the database. +#[tauri::command] +fn clear_history(state: State) -> Result<(), String> { + let db = state.db.lock().unwrap(); + db.clear().map_err(|e| e.to_string()) +} + +/// Returns the current database size in bytes and number of records. +#[tauri::command] +fn get_db_stats(state: State) -> Result<(u64, usize), String> { + let db = state.db.lock().unwrap(); + let size = db.get_db_size().map_err(|e| e.to_string())?; + let count = db.get_count().map_err(|e| e.to_string())?; + Ok((size, count)) +} + +/// Returns all detected temperature sensors. +#[tauri::command] +fn get_temperatures(state: State) -> Vec { + let mut components = state.components.lock().unwrap(); + components.refresh(true); + + components.iter().map(|c| { + TempReading { + label: c.label().to_string(), + current_celsius: c.temperature().unwrap_or(0.0), + max_celsius: c.max().unwrap_or(0.0), + critical_celsius: c.critical(), + } + }).collect() +} + +/// Returns all detected fan speeds. +#[tauri::command] +fn get_fans() -> Vec { + read_fans() +} + +/// Returns the network history for sparklines. +#[tauri::command] +fn get_network_history(state: State) -> Vec { + state.net_history.lock().unwrap().iter().cloned().collect() +} + +/// Exports a full system report as a PDF to the specified path. +#[tauri::command] +fn export_report(state: State, path: String) -> Result<(), String> { + use printpdf::*; + use std::fs::File; + use std::io::BufWriter; + + // 1. GATHER ALL DATA + let mut sys = state.sys.lock().unwrap(); + sys.refresh_all(); + + let hostname = System::host_name().unwrap_or_else(|| "Unknown".to_string()); + let os = format!("{} {}", System::name().unwrap_or_default(), System::os_version().unwrap_or_default()); + let kernel = System::kernel_version().unwrap_or_default(); + let uptime = fmt_uptime(System::uptime()); + + let cpu_brand = sys.cpus().first().map(|c| c.brand().to_string()).unwrap_or_default(); + let cpu_cores = sys.cpus().len(); + + let total_ram = sys.total_memory(); + let used_ram = sys.used_memory(); + + let battery = read_battery(); + + let mut disks_data = Vec::new(); + let disks = sysinfo::Disks::new_with_refreshed_list(); + for disk in &disks { + disks_data.push(( + disk.mount_point().to_string_lossy().to_string(), + disk.total_space(), + disk.available_space(), + )); + } + + let mut networks_data = Vec::new(); + { + let mut networks = state.networks.lock().unwrap(); + networks.refresh(true); + for (name, data) in networks.iter() { + networks_data.push((name.clone(), data.total_received(), data.total_transmitted())); + } + } + + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + + // 2. GENERATE PDF + let (doc, page1, layer1) = PdfDocument::new("Sysora Machine Report", Mm(210.0), Mm(297.0), "Base"); + let layer = doc.get_page(page1).get_layer(layer1); + + // Font selection (platform-specific fallbacks) + let font_paths = if cfg!(target_os = "windows") { + vec!["C:\\Windows\\Fonts\\arial.ttf", "C:\\Windows\\Fonts\\segoeui.ttf"] + } else if cfg!(target_os = "macos") { + vec!["/Library/Fonts/Arial.ttf", "/System/Library/Fonts/Helvetica.ttc", "/System/Library/Fonts/Cache/Avenir.ttc"] + } else { + vec![ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/freefont/FreeSans.ttf" + ] + }; + + let mut font_loaded = None; + for fp in font_paths { + if std::path::Path::new(fp).exists() { + if let Ok(f) = File::open(fp) { + if let Ok(f_ref) = doc.add_external_font(f) { + font_loaded = Some(f_ref); + break; + } + } + } + } + + let font = font_loaded.ok_or_else(|| "Could not find a suitable system font for PDF generation (DejaVuSans or Arial required).".to_string())?; + + let mut y_pos = 280.0; + + // Helper to draw text + let draw_text = |layer: &PdfLayerReference, text: &str, size: f32, x: f32, y: f32, font: &IndirectFontRef| { + layer.use_text(text, size, Mm(x), Mm(y), font); + }; + + // Header + layer.set_fill_color(Color::Rgb(Rgb::new(0.5, 0.2, 0.9, None))); // Brand purple + draw_text(&layer, "SYSORA MACHINE REPORT", 20.0, 20.0, y_pos, &font); + y_pos -= 10.0; + layer.set_fill_color(Color::Rgb(Rgb::new(0.4, 0.4, 0.4, None))); + draw_text(&layer, &format!("Generated on {} for {}", timestamp, hostname), 10.0, 20.0, y_pos, &font); + y_pos -= 20.0; + + // Sections + let draw_section_header = |layer: &PdfLayerReference, title: &str, y: &mut f32| { + layer.set_fill_color(Color::Rgb(Rgb::new(0.2, 0.2, 0.2, None))); + layer.use_text(title, 14.0, Mm(20.0), Mm(*y), &font); + *y -= 2.0; + // Line + layer.set_outline_color(Color::Rgb(Rgb::new(0.8, 0.8, 0.8, None))); + layer.set_outline_thickness(0.5); + let line = vec![(Point::new(Mm(20.0), Mm(*y)), false), (Point::new(Mm(190.0), Mm(*y)), false)]; + layer.add_line(Line { points: line, is_closed: false }); + *y -= 10.0; + }; + + let draw_row = |layer: &PdfLayerReference, label: &str, value: &str, y: &mut f32| { + layer.set_fill_color(Color::Rgb(Rgb::new(0.5, 0.5, 0.5, None))); + layer.use_text(label, 10.0, Mm(25.0), Mm(*y), &font); + layer.set_fill_color(Color::Rgb(Rgb::new(0.1, 0.1, 0.1, None))); + layer.use_text(value, 10.0, Mm(70.0), Mm(*y), &font); + *y -= 8.0; + }; + + // 1. System + draw_section_header(&layer, "SYSTEM SPECIFICATIONS", &mut y_pos); + draw_row(&layer, "Operating System", &os, &mut y_pos); + draw_row(&layer, "Kernel Version", &kernel, &mut y_pos); + draw_row(&layer, "Hostname", &hostname, &mut y_pos); + draw_row(&layer, "Uptime", &uptime, &mut y_pos); + y_pos -= 5.0; + + // 2. Processor + draw_section_header(&layer, "PROCESSOR (CPU)", &mut y_pos); + draw_row(&layer, "Model", &cpu_brand, &mut y_pos); + draw_row(&layer, "Cores", &format!("{} logical cores", cpu_cores), &mut y_pos); + y_pos -= 5.0; + + // 3. Memory + draw_section_header(&layer, "MEMORY (RAM)", &mut y_pos); + draw_row(&layer, "Total RAM", &fmt_bytes(total_ram), &mut y_pos); + draw_row(&layer, "Used RAM", &fmt_bytes(used_ram), &mut y_pos); + draw_row(&layer, "Free RAM", &fmt_bytes(total_ram - used_ram), &mut y_pos); + y_pos -= 5.0; + + // 4. Storage + draw_section_header(&layer, "STORAGE (DISKS)", &mut y_pos); + for (mount, total, avail) in disks_data { + let used = total - avail; + let pct = if total > 0 { (used as f64 / total as f64) * 100.0 } else { 0.0 }; + draw_row(&layer, &mount, &format!("{} used of {} ({:.1}%)", fmt_bytes(used), fmt_bytes(total), pct), &mut y_pos); + if y_pos < 30.0 { break; } + } + y_pos -= 5.0; + + // 5. Battery + if battery.present { + draw_section_header(&layer, "BATTERY HEALTH", &mut y_pos); + draw_row(&layer, "Health", &format!("{:.0}% β€” {}", battery.health_percent, health_label(battery.health_percent)), &mut y_pos); + draw_row(&layer, "Cycle Count", &format!("{} cycles", battery.cycle_count.unwrap_or(0)), &mut y_pos); + draw_row(&layer, "Status", &battery.status, &mut y_pos); + y_pos -= 5.0; + } + + // 6. Network + if y_pos > 40.0 { + draw_section_header(&layer, "NETWORK INTERFACES", &mut y_pos); + for (name, rx, tx) in networks_data.iter().take(5) { + draw_row(&layer, name, &format!("Total RX: {} | Total TX: {}", fmt_bytes(*rx), fmt_bytes(*tx)), &mut y_pos); + if y_pos < 30.0 { break; } + } + } + + // Footer + layer.set_fill_color(Color::Rgb(Rgb::new(0.7, 0.7, 0.7, None))); + layer.use_text("Generated by Sysora β€” Open Source System Manager", 8.0, Mm(20.0), Mm(10.0), &font); + + // Save + let file = File::create(path).map_err(|e| e.to_string())?; + doc.save(&mut BufWriter::new(file)).map_err(|e| e.to_string())?; + + Ok(()) +} + /// Returns battery info (health, charge, cycles). #[tauri::command] fn get_battery() -> BatteryInfo { @@ -649,6 +1023,17 @@ fn get_tray_snapshot(state: State) -> TraySnapshot { total_memory: sys.total_memory(), battery: read_battery(), disk_used_pct, + network: { + let mut networks = state.networks.lock().unwrap(); + networks.refresh(true); + let mut rx = 0; + let mut tx = 0; + for (_, data) in networks.iter() { + rx += data.received(); + tx += data.transmitted(); + } + (rx, tx) + } } } @@ -947,6 +1332,10 @@ fn delete_path(path: String) -> Result<(), String> { } } +fn get_db_path(app: &AppHandle) -> std::path::PathBuf { + app.path().app_data_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join("history.db") +} + fn get_settings_path(app: &AppHandle) -> std::path::PathBuf { app.path().app_data_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join("settings.json") } @@ -988,15 +1377,18 @@ async fn start_refresh_loop(app: AppHandle) { use std::time::{Instant, Duration as StdDuration}; let mut last_notification = Instant::now() - StdDuration::from_secs(60); + let mut last_db_snapshot = Instant::now() - StdDuration::from_secs(60); + let mut last_prune = Instant::now() - StdDuration::from_secs(3600); // Prune once an hour loop { - let (interval, ram_threshold, cpu_threshold) = { + let (interval, ram_threshold, cpu_threshold, temp_threshold) = { let state = app.state::(); let settings = state.settings.lock().unwrap(); ( settings.refresh_interval_secs, settings.ram_alert_threshold, settings.cpu_alert_threshold, + settings.temp_threshold, ) }; @@ -1023,12 +1415,39 @@ async fn start_refresh_loop(app: AppHandle) { 0.0 }; + // Check Network usage + let mut total_rx = 0; + let mut total_tx = 0; + { + let mut networks = state.networks.lock().unwrap(); + networks.refresh(true); + for (_, data) in networks.iter() { + total_rx += data.received(); + total_tx += data.transmitted(); + } + } + + // Check Temperatures + let mut max_temp: f32 = 0.0; + { + let mut components = state.components.lock().unwrap(); + components.refresh(true); + for c in components.iter() { + if let Some(t) = c.temperature() { + if t > max_temp { + max_temp = t; + } + } + } + } + // Record history point + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + { - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); let mut hist = state.history.lock().unwrap(); hist.push_back(SnapPoint { ts, @@ -1040,6 +1459,19 @@ async fn start_refresh_loop(app: AppHandle) { } } + // Record network history + { + let mut net_hist = state.net_history.lock().unwrap(); + net_hist.push_back(NetworkHistory { + ts, + rx_speed: total_rx, + tx_speed: total_tx, + }); + if net_hist.len() > 60 { + net_hist.pop_front(); + } + } + if last_notification.elapsed() > StdDuration::from_secs(60) { let mut triggered = false; if cpu_usage > cpu_threshold { @@ -1056,6 +1488,13 @@ async fn start_refresh_loop(app: AppHandle) { .body(format!("RAM usage is at {:.1}%", ram_usage)) .show(); triggered = true; + } else if max_temp > temp_threshold { + let _ = app.notification() + .builder() + .title("High Temperature Alert") + .body(format!("Hardware sensor reached {:.1}Β°C", max_temp)) + .show(); + triggered = true; } if triggered { @@ -1064,6 +1503,48 @@ async fn start_refresh_loop(app: AppHandle) { } let _ = app.emit("process-update", ()); + let _ = app.emit("network-update", ()); + + // ─── Database recording (1m interval) ─── + if last_db_snapshot.elapsed() >= StdDuration::from_secs(60) { + let mut disks = sysinfo::Disks::new_with_refreshed_list(); + disks.refresh(true); + let mut disk_used = 0; + let mut disk_total = 0; + for d in disks.iter() { + disk_total += d.total_space(); + disk_used += d.total_space() - d.available_space(); + } + + let max_temp = { + let mut comps = state.components.lock().unwrap(); + comps.refresh(true); + comps.iter().filter_map(|c| c.temperature()).fold(0.0, f32::max) + }; + + let point = HistoricalPoint { + ts, + cpu_pct: cpu_usage, + ram_used: used_mem, + ram_total: total_mem, + disk_used, + disk_total, + cpu_temp: max_temp, + }; + + { + let db = state.db.lock().unwrap(); + let _ = db.save_snapshot(&point); + } + last_db_snapshot = Instant::now(); + } + + // ─── Auto-pruning (30 days) ─── + if last_prune.elapsed() >= StdDuration::from_secs(3600) { + let db = state.db.lock().unwrap(); + let _ = db.prune(30); + last_prune = Instant::now(); + } } } @@ -1071,11 +1552,31 @@ async fn start_refresh_loop(app: AppHandle) { fn get_app_icon_data_url(path: String) -> Result { if path.is_empty() { return Ok(String::new()); } + let path_lc = path.to_lowercase(); + + #[cfg(target_os = "windows")] + { + // If it's an executable or doesn't have a typical image extension, try windows_icons + let is_image = path_lc.ends_with(".png") || path_lc.ends_with(".jpg") || + path_lc.ends_with(".jpeg") || path_lc.ends_with(".svg") || + path_lc.ends_with(".ico"); + + if !is_image || path_lc.ends_with(".exe") || path_lc.ends_with(".dll") { + let b64 = windows_icons::get_icon_base64_by_path(&path); + if !b64.is_empty() { + if b64.starts_with("data:") { + return Ok(b64); + } else { + return Ok(format!("data:image/png;base64,{}", b64)); + } + } + } + } + use base64::{Engine as _, engine::general_purpose}; let bytes = std::fs::read(&path).map_err(|e| e.to_string())?; let b64 = general_purpose::STANDARD.encode(bytes); - let path_lc = path.to_lowercase(); let mime = if path_lc.ends_with(".svg") { "image/svg+xml" } else if path_lc.ends_with(".png") { @@ -1099,6 +1600,7 @@ fn get_app_icon_data_url(path: String) -> Result { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--minimized"]))) .plugin(tauri_plugin_notification::init()) .on_window_event(|window, event| match event { @@ -1114,12 +1616,22 @@ pub fn run() { let args: Vec = std::env::args().collect(); let is_minimized = args.iter().any(|a| a == "--minimized") || start_minimized; + let db_path = get_db_path(app.handle()); + if let Some(parent) = db_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let db_manager = DbManager::new(db_path).expect("Failed to initialize database"); + app.manage(SysState { sys: Mutex::new(System::new_all()), settings: Mutex::new(settings), history: Mutex::new(VecDeque::with_capacity(60)), scan_results: Mutex::new(Vec::new()), is_scanning: Mutex::new(false), + networks: Mutex::new(sysinfo::Networks::new_with_refreshed_list()), + net_history: Mutex::new(VecDeque::with_capacity(60)), + components: Mutex::new(sysinfo::Components::new_with_refreshed_list()), + db: Mutex::new(db_manager), }); // Force the window icon for Linux dock @@ -1212,6 +1724,10 @@ pub fn run() { get_system_info, get_disks, get_battery, + get_network_stats, + get_network_history, + get_temperatures, + export_report, get_tray_snapshot, get_installed_apps, uninstall_app, @@ -1222,6 +1738,10 @@ pub fn run() { get_scan_results, delete_path, get_history, + get_fans, + get_historical_trends, + clear_history, + get_db_stats, ]) .run(tauri::generate_context!()) .expect("error while running sysora"); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2326f1f..de1272a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,27 @@ // Prevents additional console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod cli; + +use clap::Parser; + fn main() { + let args: Vec = std::env::args().collect(); + + // If we have more than 1 argument, or the first argument is not the binary path (unlikely), + // we assume CLI mode. Note: Tauri might pass some args, so we check specifically. + if args.len() > 1 { + // If the only argument is --minimized, we let it fall through to the GUI + if args.len() == 2 && args[1] == "--minimized" { + // Proceed to GUI + } else { + // For any other arguments, we treat it as a CLI call. + // .parse() will print help/errors and exit the process. + let parsed = cli::Cli::parse(); + cli::run_cli(parsed); + return; + } + } + sysora_lib::run(); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9d501dd..4f3733f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Sysora", - "version": "0.2.0", + "version": "0.3.0", "identifier": "sysora", "build": { "frontendDist": "../dist", diff --git a/src/components/charts/HistoryChart.tsx b/src/components/charts/HistoryChart.tsx index eaf5b6a..eb807eb 100644 --- a/src/components/charts/HistoryChart.tsx +++ b/src/components/charts/HistoryChart.tsx @@ -31,20 +31,20 @@ export function HistoryChart() { })); return ( -
+
-
- +
+

60s Resource History

-
- CPU +
+ CPU
- RAM + RAM
@@ -62,20 +62,35 @@ export function HistoryChart() { - + `${val}%`} /> [`${val.toFixed(1)}%`]} + content={({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry: any, index: number) => ( +
+
+ + {entry.name}: {entry.value?.toFixed(1)}% + +
+ ))} +
+ ); + } + return null; + }} /> + + Warming up network history... +
+ ); + } + + const chartData = history.map((p) => ({ + ...p, + rx_speed_kb: p.rx_speed / 1024, + tx_speed_kb: p.tx_speed / 1024, + time: new Date(p.ts * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + })); + + return ( +
+
+
+ +

60s Network Activity

+
+
+
+
+ Download +
+
+
+ Upload +
+
+
+ +
+ + + + + + + + + + + + + + + `${(val/1024).toFixed(1)}M`} + /> + { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry: any, index: number) => ( +
+
+ + {entry.name}: {fmtBytes(entry.value * 1024)}/s + +
+ ))} +
+ ); + } + return null; + }} + /> + + + + +
+
+ ); +} diff --git a/src/components/layout/Shell.tsx b/src/components/layout/Shell.tsx index fc3b832..fc86265 100644 --- a/src/components/layout/Shell.tsx +++ b/src/components/layout/Shell.tsx @@ -1,26 +1,56 @@ +import { useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; import { Sidebar } from "./Sidebar"; import { TopBar } from "./TopBar"; import { MemoryTab } from "@/components/tabs/MemoryTab"; import { ProcessesTab } from "@/components/tabs/ProcessesTab"; +import { NetworkTab } from "@/components/tabs/NetworkTab"; import { AppsTab } from "@/components/tabs/AppsTab"; import { DiskTab } from "@/components/tabs/DiskTab"; import { SystemInfoTab } from "@/components/tabs/SystemInfoTab"; +import { HistoryTab } from "@/components/tabs/HistoryTab"; import { SettingsTab } from "@/components/tabs/SettingsTab"; import { useAppStore } from "@/store/app"; +import { api } from "@/lib/api"; export function Shell() { const activeTab = useAppStore((s) => s.activeTab); + const { data: settings } = useQuery({ queryKey: ["settings"], queryFn: api.getSettings }); + + useEffect(() => { + if (!settings) return; + + const root = window.document.documentElement; + const applyTheme = (t: string) => { + if (t === "dark" || (t === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches)) { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } + }; + + applyTheme(settings.theme); + + if (settings.theme === "system") { + const media = window.matchMedia("(prefers-color-scheme: dark)"); + const listener = () => applyTheme("system"); + media.addEventListener("change", listener); + return () => media.removeEventListener("change", listener); + } + }, [settings?.theme]); return ( -
+
{activeTab === "memory" && } {activeTab === "processes" && } + {activeTab === "network" && } {activeTab === "apps" && } {activeTab === "disk" && } + {activeTab === "history" && } {activeTab === "system-info" && } {activeTab === "settings" && }
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 6166dd5..df98f45 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,6 +1,6 @@ import { Activity, List, AppWindow, HardDrive, - Info, Settings, Cpu, + Info, Settings, Cpu, Globe, History } from "lucide-react"; import { useAppStore } from "@/store/app"; import type { TabId } from "@/types"; @@ -9,6 +9,8 @@ const NAV = [ { section: "Monitor" }, { id: "memory", label: "Memory", icon: Activity }, { id: "processes", label: "Processes", icon: List }, + { id: "network", label: "Network", icon: Globe }, + { id: "history", label: "History", icon: History }, { section: "Manage" }, { id: "apps", label: "Applications", icon: AppWindow }, { id: "disk", label: "Disk Scanner", icon: HardDrive }, @@ -21,16 +23,16 @@ export function Sidebar() { const { activeTab, setActiveTab } = useAppStore(); return ( -