diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..ea422c6
Binary files /dev/null and b/.DS_Store differ
diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml
index 3d198c2..2d0d0a3 100644
--- a/.github/workflows/rust.yaml
+++ b/.github/workflows/rust.yaml
@@ -11,8 +11,8 @@ on:
jobs:
#run build first to populate caches
- build:
- name: Build binary
+ build-windows:
+ name: Build Windows binary
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
@@ -34,50 +34,91 @@ jobs:
with:
command: build
args: --workspace --all-targets --all-features --release
+
+
+
- name: Check for release
id: is-release
shell: bash
run: |
unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi
echo ::set-output name=IS_RELEASE::${IS_RELEASE}
- - name: Artifact Hash
- id: package
- shell: bash
- run: |
- md5sum target/release/rust_cat.exe >> hashes.txt
- sha256sum target/release/rust_cat.exe >> hashes.txt
- cat hashes.txt
- - name: Artifact upload
+
+ - name: Artifact upload (Windows)
uses: actions/upload-artifact@master
with:
name: rust_cat.exe
path: target/release/rust_cat.exe
- - name: Hash upload
+
+
+
+ - name: Publish archives and packages (Windows)
+ uses: softprops/action-gh-release@v1
+ if: steps.is-release.outputs.IS_RELEASE
+ with:
+ files: |
+ target/release/rust_cat.exe
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+
+ # Create universal macOS app bundle
+ build-macos:
+ name: Create universal macOS app bundle
+ runs-on: macos-latest
+ if: github.event_name != 'pull_request' || github.actor != 'dependabot[bot]'
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Run macOS build script
+ run: ./build_macos.sh
+
+ - name: Check for release
+ id: is-release
+ shell: bash
+ run: |
+ unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi
+ echo ::set-output name=IS_RELEASE::${IS_RELEASE}
+
+ - name: Upload universal app bundle
+ uses: actions/upload-artifact@master
+ with:
+ name: RustCat-universal.app.zip
+ path: RustCat-universal.app.zip
+
+ - name: Upload universal DMG
uses: actions/upload-artifact@master
with:
- name: hashes.txt
- path: hashes.txt
- - name: Publish archives and packages
+ name: RustCat-universal.dmg
+ path: RustCat-universal.dmg
+
+ - name: Publish universal macOS release
uses: softprops/action-gh-release@v1
if: steps.is-release.outputs.IS_RELEASE
with:
files: |
- target/release/rust_cat.exe
- hashes.txt
+ RustCat-universal.app.zip
+ RustCat-universal.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Auto-approve Dependabot PRs if build succeeds
dependabot-auto-approve:
runs-on: windows-latest
- needs: build
+ needs: [build-windows, build-macos]
if: github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'
steps:
- name: Auto-approve PR
uses: hmarr/auto-approve-action@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
-
+
- name: Enable auto-merge
run: |
gh pr merge --auto --squash "${{ github.event.pull_request.number }}"
diff --git a/.gitignore b/.gitignore
index 98aacae..cf6daac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,28 @@
/target
-.claude
\ No newline at end of file
+.claude
+
+# macOS build artifacts
+*.app
+*.dmg
+*.app.zip
+*-universal.app
+*-universal.dmg
+*-universal.app.zip
+rust_cat_universal
+*.icns
+*.iconset/
+
+# macOS system files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Build artifacts
+RustCat.app/
+RustCat-*.app/
+RustCat-*.dmg
+RustCat-*.app.zip
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 53a1220..e02161a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,20 +4,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-RustCat is a Windows tray application that displays an animated cat or parrot icon whose animation speed reflects real-time CPU usage. It's a Rust port of RunCat_for_windows, removing the .NET runtime dependency.
+RustCat is a cross-platform tray application that displays an animated cat or parrot icon whose animation speed reflects real-time CPU usage. Originally a Windows-only Rust port of RunCat_for_windows, it now supports both Windows and macOS.
## Build and Development Commands
### Building
```bash
-# In WSL, use cargo.exe to build Windows binaries
+# Windows (In WSL, use cargo.exe to build Windows binaries)
cargo.exe build --release
+
+# macOS
+cargo build --release
```
### Running (Development)
```bash
-# In WSL, use cargo.exe to run Windows binaries
+# Windows (In WSL, use cargo.exe to run Windows binaries)
cargo.exe run
+
+# macOS
+cargo run
```
### Testing
@@ -69,12 +75,19 @@ Icons are organized in `assets/` directory:
### Platform Specifics
-- Windows-only application using Windows API extensively via windows-rs crate
+#### Windows
+- Uses Windows API extensively via windows-rs crate
- Uses Windows subsystem (no console window in release builds)
- Integrates with Windows registry for settings and startup behavior
-- Requires Windows for tray icon functionality and CPU monitoring
- When developing in WSL, use `cargo.exe` instead of `cargo` to build Windows binaries
+#### macOS
+- Uses macOS NSApplication for proper integration
+- Uses macOS defaults command for settings persistence
+- Uses macOS LaunchAgents for startup behavior
+- Detects system dark/light theme automatically
+- Opens Activity Monitor instead of Task Manager
+
## Development Environment Notes
- When running in WSL, use `cargo.exe` to build
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 1464d2d..ad510cd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,21 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.1"
@@ -23,6 +38,27 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "flate2"
version = "1.1.2"
@@ -33,6 +69,33 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libredox"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -42,6 +105,111 @@ dependencies = [
"adler2",
]
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -60,11 +228,26 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
[[package]]
name = "rust_cat"
version = "2.1.0"
dependencies = [
+ "dirs",
"flate2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
"trayicon",
"windows",
"winreg",
@@ -102,6 +285,26 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "toml"
version = "0.5.11"
@@ -114,9 +317,11 @@ dependencies = [
[[package]]
name = "trayicon"
version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c51617694e059fe4b83ab48e435660c1ba5b48b1573b4c143fdabd1c0f279daa"
+source = "git+https://github.com/bearice/trayicon-rs.git#cd6ff5ea862e08dd6caed74e0aa6a4e662f21306"
dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
"winapi",
]
@@ -126,6 +331,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "winapi"
version = "0.3.9"
@@ -250,13 +461,37 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
]
[[package]]
@@ -265,14 +500,14 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
]
[[package]]
@@ -284,18 +519,36 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -308,24 +561,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -339,7 +616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
- "windows-sys",
+ "windows-sys 0.59.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index d59653b..e3a1d9d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,11 +6,19 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-trayicon = "0.2.0"
-winreg = "0.55.0"
+trayicon = { git = "https://github.com/bearice/trayicon-rs.git" }
flate2 = "1.0"
+dirs = "5.0"
+
+[target.'cfg(windows)'.dependencies]
+winreg = "0.55.0"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+objc2 = "0.5"
+objc2-app-kit = { version = "0.2", features = ["all"] }
+objc2-foundation = { version = "0.2", features = ["all"] }
-[dependencies.windows]
+[target.'cfg(windows)'.dependencies.windows]
version = "0.61"
features = [
"Win32_Foundation",
@@ -28,9 +36,11 @@ codegen-units = 1 # Reduce code generation units
panic = "abort" # Reduce panic code size
[build-dependencies]
-winres = "0.1"
flate2 = "1.0"
+[target.'cfg(windows)'.build-dependencies]
+winres = "0.1"
+
[package.metadata.winres]
OriginalFilename = "rust_cat.exe"
FileDescription = "π» => ππΊπΈπΉπ»πΌπ½πββ¬"
diff --git a/Info.plist b/Info.plist
new file mode 100644
index 0000000..648027d
--- /dev/null
+++ b/Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ CFBundleName
+ RustCat
+ CFBundleDisplayName
+ RustCat
+ CFBundleIdentifier
+ com.bearice.rustcat
+ CFBundleVersion
+ 2.1.0
+ CFBundleShortVersionString
+ 2.1.0
+ CFBundlePackageType
+ APPL
+ CFBundleExecutable
+ rust_cat
+ CFBundleIconFile
+ AppIcon
+ LSUIElement
+
+ NSHighResolutionCapable
+
+ NSHumanReadableCopyright
+ Copyright Β© 2021 Bearice Ren
+ CFBundleInfoDictionaryVersion
+ 6.0
+ LSMinimumSystemVersion
+ 10.15
+ NSPrincipalClass
+ NSApplication
+
+
\ No newline at end of file
diff --git a/build.rs b/build.rs
index 7ddff06..fcb8a36 100644
--- a/build.rs
+++ b/build.rs
@@ -1,176 +1,198 @@
-use std::{io, path::Path, collections::HashMap};
-use winres::WindowsResource;
-use flate2::{write::GzEncoder, Compression};
-
-fn main() -> io::Result<()> {
- // Get Git commit hash
- let output = std::process::Command::new("git")
- .args(["rev-parse", "--short", "HEAD"])
- .output();
-
- match output {
- Ok(output) if output.status.success() => {
- let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
- if !git_hash.is_empty() {
- println!("cargo:rustc-env=GIT_HASH={}", git_hash);
- } else {
- println!("cargo:rustc-env=GIT_HASH=N/A");
- }
- }
- _ => {
- println!("cargo:rustc-env=GIT_HASH=N/A");
- }
- }
-
- let profile = std::env::var("PROFILE").unwrap();
- if profile == "release" {
- println!("cargo:rustc-cfg=release");
- }
- let mut res = WindowsResource::new();
- // This path can be absolute, or relative to your crate root.
- res.set_icon("assets/appIcon.ico");
-
- res.compile()?;
- generate_icon_resources()?;
- Ok(())
-}
-
-fn generate_icon_resources() -> io::Result<()> {
- let out_dir = std::env::var_os("OUT_DIR").unwrap();
- let dest_path = Path::new(&out_dir).join("icon_data.rs");
-
- // Define icon configurations that match the icon manager structure
- let icon_configs = [
- ("cat", [("light", 5), ("dark", 5)]),
- ("parrot", [("light", 10), ("dark", 10)]),
- ];
-
- // Concatenate ALL icons into one big chunk for maximum compression
- let (compressed_data, icon_metadata) = generate_all_icons_compressed(&icon_configs)?;
-
- // Generate code with single compressed chunk
- let code = generate_single_chunk_module(&compressed_data, &icon_metadata);
-
- std::fs::write(&dest_path, code.as_bytes())
-}
-
-// Icon metadata for the single compressed chunk
-#[derive(Debug)]
-struct IconGroupMetadata {
- icon_name: String,
- theme: String,
- offset: usize,
- sizes: Vec,
-}
-
-fn generate_all_icons_compressed(icon_configs: &[(&str, [(&str, usize); 2])]) -> io::Result<(String, Vec)> {
- let mut all_icons_data = Vec::new();
- let mut metadata = Vec::new();
-
- // Collect all icon data
- for (icon_name, themes) in icon_configs.iter() {
- for (theme, count) in themes.iter() {
- let base = std::fs::canonicalize(Path::new("assets").join(icon_name))?;
- let icon_file_names = (0..*count)
- .map(|i| format!("{}_{}_{}", theme, icon_name, i))
- .collect::>();
-
- let mut group_sizes = Vec::new();
- let group_offset = all_icons_data.len();
-
- // Read all icons for this group
- for name in &icon_file_names {
- let icon_path = base.join(format!("{}.ico", name));
- let icon_data = std::fs::read(&icon_path)?;
- group_sizes.push(icon_data.len());
- all_icons_data.extend_from_slice(&icon_data);
- }
-
- metadata.push(IconGroupMetadata {
- icon_name: icon_name.to_string(),
- theme: theme.to_string(),
- offset: group_offset,
- sizes: group_sizes,
- });
- }
- }
-
- // Compress all icons together
- let mut gz_encoder = GzEncoder::new(Vec::new(), Compression::best());
- std::io::copy(&mut &all_icons_data[..], &mut gz_encoder)?;
- let compressed = gz_encoder.finish()?;
-
- // Write compressed data to OUT_DIR
- let out_dir = std::env::var_os("OUT_DIR").unwrap();
- let compressed_path = Path::new(&out_dir).join("all_icons.gz");
- std::fs::write(&compressed_path, &compressed)?;
-
- println!("cargo:info=All icons compressed: {} bytes -> {} bytes ({:.1}% reduction)",
- all_icons_data.len(), compressed.len(),
- 100.0 * (1.0 - compressed.len() as f64 / all_icons_data.len() as f64));
-
- Ok((compressed_path.display().to_string(), metadata))
-}
-
-fn generate_single_chunk_module(compressed_path: &str, metadata: &[IconGroupMetadata]) -> String {
- let mut code = String::new();
-
- code.push_str("// All icons compressed into a single chunk for maximum compression\n");
- code.push_str(&format!("pub const ALL_ICONS_COMPRESSED: &[u8] = include_bytes!(r\"{}\");\n\n", compressed_path));
-
- code.push_str("use std::collections::HashMap;\n\n");
-
- // IconGroupInfo is now defined in main.rs
-
- // Generate size arrays for each group
- for meta in metadata {
- let sizes_array = meta.sizes
- .iter()
- .map(|size| size.to_string())
- .collect::>()
- .join(", ");
-
- code.push_str(&format!(
- "const {}_{}_{}_SIZES: &[u32] = &[{}];\n",
- meta.theme.to_uppercase(),
- meta.icon_name.to_uppercase(),
- "INDIVIDUAL",
- sizes_array
- ));
- }
-
- code.push('\n');
-
- // Generate function to get icon metadata
- code.push_str("pub fn get_icon_metadata() -> IconData {\n");
- code.push_str(" let mut icons = HashMap::new();\n\n");
-
- // Group metadata by icon name
- let mut grouped_metadata: HashMap> = HashMap::new();
- for meta in metadata {
- grouped_metadata.entry(meta.icon_name.clone()).or_default().push(meta);
- }
-
- for (icon_name, themes) in grouped_metadata {
- code.push_str(&format!(" // {} icons\n", icon_name));
- code.push_str(&format!(" let mut {} = HashMap::new();\n", icon_name));
-
- for meta in themes {
- code.push_str(&format!(
- " {}.insert(\"{}\", IconGroupInfo {{ offset: {}, sizes: {}_{}_INDIVIDUAL_SIZES }});\n",
- icon_name,
- meta.theme,
- meta.offset,
- meta.theme.to_uppercase(),
- meta.icon_name.to_uppercase()
- ));
- }
-
- code.push_str(&format!(" icons.insert(\"{}\", {});\n\n", icon_name, icon_name));
- }
-
- code.push_str(" icons\n");
- code.push_str("}\n");
-
- code
-}
+use flate2::{write::GzEncoder, Compression};
+use std::{collections::HashMap, io, path::Path};
+
+// only include winres if compiling for Windows
+#[cfg(target_os = "windows")]
+use winres::WindowsResource;
+
+fn main() -> io::Result<()> {
+ // Get Git commit hash
+ let output = std::process::Command::new("git")
+ .args(["rev-parse", "--short", "HEAD"])
+ .output();
+
+ match output {
+ Ok(output) if output.status.success() => {
+ let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !git_hash.is_empty() {
+ println!("cargo:rustc-env=GIT_HASH={}", git_hash);
+ } else {
+ println!("cargo:rustc-env=GIT_HASH=N/A");
+ }
+ }
+ _ => {
+ println!("cargo:rustc-env=GIT_HASH=N/A");
+ }
+ }
+
+ let profile = std::env::var("PROFILE").unwrap();
+ if profile == "release" {
+ println!("cargo:rustc-cfg=release");
+ }
+
+ // Generate Windows resource file if compiling for Windows
+ #[cfg(target_os = "windows")]
+ {
+ let mut res = WindowsResource::new();
+ res.set_icon("assets/appIcon.ico");
+ res.compile()?;
+ }
+
+ generate_icon_resources()?;
+ Ok(())
+}
+
+fn generate_icon_resources() -> io::Result<()> {
+ let out_dir = std::env::var_os("OUT_DIR").unwrap();
+ let dest_path = Path::new(&out_dir).join("icon_data.rs");
+
+ // Define icon configurations that match the icon manager structure
+ let icon_configs = [
+ ("cat", [("light", 5), ("dark", 5)]),
+ ("parrot", [("light", 10), ("dark", 10)]),
+ ];
+
+ // Concatenate ALL icons into one big chunk for maximum compression
+ let (compressed_data, icon_metadata) = generate_all_icons_compressed(&icon_configs)?;
+
+ // Generate code with single compressed chunk
+ let code = generate_single_chunk_module(&compressed_data, &icon_metadata);
+
+ std::fs::write(&dest_path, code.as_bytes())
+}
+
+// Icon metadata for the single compressed chunk
+#[derive(Debug)]
+struct IconGroupMetadata {
+ icon_name: String,
+ theme: String,
+ offset: usize,
+ sizes: Vec,
+}
+
+fn generate_all_icons_compressed(
+ icon_configs: &[(&str, [(&str, usize); 2])],
+) -> io::Result<(String, Vec)> {
+ let mut all_icons_data = Vec::new();
+ let mut metadata = Vec::new();
+
+ // Collect all icon data
+ for (icon_name, themes) in icon_configs.iter() {
+ for (theme, count) in themes.iter() {
+ let base = std::fs::canonicalize(Path::new("assets").join(icon_name))?;
+ let icon_file_names = (0..*count)
+ .map(|i| format!("{}_{}_{}", theme, icon_name, i))
+ .collect::>();
+
+ let mut group_sizes = Vec::new();
+ let group_offset = all_icons_data.len();
+
+ // Read all icons for this group
+ for name in &icon_file_names {
+ let icon_path = base.join(format!("{}.ico", name));
+ let icon_data = std::fs::read(&icon_path)?;
+ group_sizes.push(icon_data.len());
+ all_icons_data.extend_from_slice(&icon_data);
+ }
+
+ metadata.push(IconGroupMetadata {
+ icon_name: icon_name.to_string(),
+ theme: theme.to_string(),
+ offset: group_offset,
+ sizes: group_sizes,
+ });
+ }
+ }
+
+ // Compress all icons together
+ let mut gz_encoder = GzEncoder::new(Vec::new(), Compression::best());
+ std::io::copy(&mut &all_icons_data[..], &mut gz_encoder)?;
+ let compressed = gz_encoder.finish()?;
+
+ // Write compressed data to OUT_DIR
+ let out_dir = std::env::var_os("OUT_DIR").unwrap();
+ let compressed_path = Path::new(&out_dir).join("all_icons.gz");
+ std::fs::write(&compressed_path, &compressed)?;
+
+ println!(
+ "cargo:info=All icons compressed: {} bytes -> {} bytes ({:.1}% reduction)",
+ all_icons_data.len(),
+ compressed.len(),
+ 100.0 * (1.0 - compressed.len() as f64 / all_icons_data.len() as f64)
+ );
+
+ Ok((compressed_path.display().to_string(), metadata))
+}
+
+fn generate_single_chunk_module(compressed_path: &str, metadata: &[IconGroupMetadata]) -> String {
+ let mut code = String::new();
+
+ code.push_str("// All icons compressed into a single chunk for maximum compression\n");
+ code.push_str(&format!(
+ "pub const ALL_ICONS_COMPRESSED: &[u8] = include_bytes!(r\"{}\");\n\n",
+ compressed_path
+ ));
+
+ code.push_str("use std::collections::HashMap;\n\n");
+
+ // IconGroupInfo is now defined in main.rs
+
+ // Generate size arrays for each group
+ for meta in metadata {
+ let sizes_array = meta
+ .sizes
+ .iter()
+ .map(|size| size.to_string())
+ .collect::>()
+ .join(", ");
+
+ code.push_str(&format!(
+ "const {}_{}_{}_SIZES: &[u32] = &[{}];\n",
+ meta.theme.to_uppercase(),
+ meta.icon_name.to_uppercase(),
+ "INDIVIDUAL",
+ sizes_array
+ ));
+ }
+
+ code.push('\n');
+
+ // Generate function to get icon metadata
+ code.push_str("pub fn get_icon_metadata() -> IconData {\n");
+ code.push_str(" let mut icons = HashMap::new();\n\n");
+
+ // Group metadata by icon name
+ let mut grouped_metadata: HashMap> = HashMap::new();
+ for meta in metadata {
+ grouped_metadata
+ .entry(meta.icon_name.clone())
+ .or_default()
+ .push(meta);
+ }
+
+ for (icon_name, themes) in grouped_metadata {
+ code.push_str(&format!(" // {} icons\n", icon_name));
+ code.push_str(&format!(" let mut {} = HashMap::new();\n", icon_name));
+
+ for meta in themes {
+ code.push_str(&format!(
+ " {}.insert(\"{}\", IconGroupInfo {{ offset: {}, sizes: {}_{}_INDIVIDUAL_SIZES }});\n",
+ icon_name,
+ meta.theme,
+ meta.offset,
+ meta.theme.to_uppercase(),
+ meta.icon_name.to_uppercase()
+ ));
+ }
+
+ code.push_str(&format!(
+ " icons.insert(\"{}\", {});\n\n",
+ icon_name, icon_name
+ ));
+ }
+
+ code.push_str(" icons\n");
+ code.push_str("}\n");
+
+ code
+}
diff --git a/build_app_icon.sh b/build_app_icon.sh
new file mode 100755
index 0000000..a65ff15
--- /dev/null
+++ b/build_app_icon.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# Script to create a macOS app icon from existing assets
+# This is optional and will be run during CI if iconutil is available
+
+if command -v iconutil &> /dev/null; then
+ echo "Creating app icon..."
+
+ # Create iconset directory
+ mkdir -p RustCat.iconset
+
+ # We'll use the main app icon
+ # Convert ICO to PNG at required sizes using sips (macOS built-in tool)
+ if command -v sips &> /dev/null; then
+ sips -s format png -Z 16 assets/appIcon.ico --out RustCat.iconset/icon_16x16.png
+ sips -s format png -Z 32 assets/appIcon.ico --out RustCat.iconset/icon_32x32.png
+ sips -s format png -Z 128 assets/appIcon.ico --out RustCat.iconset/icon_128x128.png
+ sips -s format png -Z 256 assets/appIcon.ico --out RustCat.iconset/icon_256x256.png
+ sips -s format png -Z 512 assets/appIcon.ico --out RustCat.iconset/icon_512x512.png
+ else
+ echo "sips not found"
+ exit 1
+ fi
+
+ # Create the .icns file
+ iconutil -c icns RustCat.iconset
+
+ # Clean up
+ rm -rf RustCat.iconset
+
+ echo "App icon created: RustCat.icns"
+else
+ echo "iconutil not found, skipping app icon creation"
+fi
diff --git a/build_macos.sh b/build_macos.sh
new file mode 100755
index 0000000..e2104c9
--- /dev/null
+++ b/build_macos.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+
+# macOS build script for RustCat
+# This script handles all macOS-specific build tasks including:
+# - Building for both Intel and Apple Silicon
+# - Creating universal binaries
+# - Building app bundles
+# - Creating DMG images
+
+set -e
+
+echo "π Starting macOS build process..."
+
+# Configuration
+APP_NAME="RustCat"
+BUNDLE_ID="com.bearice.rustcat"
+VERSION=$(grep '^version' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
+
+# Build targets
+INTEL_TARGET="x86_64-apple-darwin"
+ARM_TARGET="aarch64-apple-darwin"
+
+# Ensure targets are installed
+echo "π¦ Installing build targets..."
+rustup target add $INTEL_TARGET
+rustup target add $ARM_TARGET
+
+# Build for both architectures
+echo "π¨ Building for Intel (x86_64)..."
+cargo build --release --target $INTEL_TARGET
+
+echo "π¨ Building for Apple Silicon (aarch64)..."
+cargo build --release --target $ARM_TARGET
+
+# Create universal binary
+echo "π Creating universal binary..."
+lipo -create -output rust_cat_universal \
+ target/$INTEL_TARGET/release/rust_cat \
+ target/$ARM_TARGET/release/rust_cat
+
+# Verify universal binary
+echo "β
Verifying universal binary..."
+lipo -info rust_cat_universal
+file rust_cat_universal
+
+# Create app icon
+echo "π¨ Creating app icon..."
+./build_app_icon.sh || echo "β οΈ Could not create app icon, continuing without it"
+
+# Create app bundle
+echo "π± Creating app bundle..."
+APP_BUNDLE="${APP_NAME}.app"
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+mkdir -p "$APP_BUNDLE/Contents/Resources"
+
+# Copy binary
+cp rust_cat_universal "$APP_BUNDLE/Contents/MacOS/rust_cat"
+
+# Copy Info.plist
+cp Info.plist "$APP_BUNDLE/Contents/"
+
+# Copy app icon if it exists
+if [ -f "RustCat.icns" ]; then
+ cp RustCat.icns "$APP_BUNDLE/Contents/Resources/AppIcon.icns"
+ echo "β
App icon added"
+else
+ echo "β οΈ No app icon found"
+fi
+
+# Make binary executable
+chmod +x "$APP_BUNDLE/Contents/MacOS/rust_cat"
+
+# Create ZIP archive
+echo "π¦ Creating ZIP archive..."
+zip -r "${APP_NAME}-universal.app.zip" "$APP_BUNDLE"
+
+# Create DMG
+echo "πΏ Creating DMG..."
+./create_dmg.sh "$APP_BUNDLE" "${APP_NAME}-universal.dmg"
+
+# Clean up temporary files
+echo "π§Ή Cleaning up..."
+rm -f rust_cat_universal
+rm -f RustCat.icns
+rm -rf RustCat.iconset
+
+echo "β
macOS build complete\!"
+echo ""
+echo "π¦ Created files:"
+echo " - ${APP_NAME}-universal.app.zip (Universal app bundle)"
+echo " - ${APP_NAME}-universal.dmg (DMG installer)"
+echo ""
+echo "π Universal binary info:"
+lipo -info "$APP_BUNDLE/Contents/MacOS/rust_cat"
diff --git a/create_dmg.sh b/create_dmg.sh
new file mode 100755
index 0000000..3da1a60
--- /dev/null
+++ b/create_dmg.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Script to create a DMG image for macOS app distribution
+# Usage: ./create_dmg.sh
+
+APP_PATH="$1"
+DMG_NAME="$2"
+
+if [ -z "$APP_PATH" ] || [ -z "$DMG_NAME" ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+if [ ! -d "$APP_PATH" ]; then
+ echo "Error: App bundle not found at $APP_PATH"
+ exit 1
+fi
+
+# Create a temporary directory for DMG contents
+TEMP_DIR=$(mktemp -d)
+DMG_DIR="$TEMP_DIR/dmg_contents"
+mkdir -p "$DMG_DIR"
+
+# Copy the app bundle to the DMG directory
+cp -R "$APP_PATH" "$DMG_DIR/"
+
+# Create a symbolic link to Applications folder for easy installation
+ln -s /Applications "$DMG_DIR/Applications"
+
+# Create the DMG
+hdiutil create -volname "RustCat" -srcfolder "$DMG_DIR" -ov -format UDZO "$DMG_NAME"
+
+# Clean up
+rm -rf "$TEMP_DIR"
+
+echo "DMG created: $DMG_NAME"
diff --git a/src/app.rs b/src/app.rs
index 0244fd1..3ddf9ee 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,210 +1,192 @@
-use std::{
- sync::{
- atomic::{AtomicBool, Ordering},
- Arc, Mutex,
- },
- thread::sleep,
- time::Duration,
-};
-use trayicon::*;
-use windows::Win32::UI::WindowsAndMessaging::*;
-
-use crate::{
- cpu_usage,
- events::{Events, build_menu},
- icon_manager::IconManager,
- settings::{set_current_icon, set_current_theme, get_current_icon, get_current_theme, is_run_on_start_enabled, set_run_on_start},
- windows_api::{safe_message_box, safe_shell_execute, safe_message_loop},
-};
-
-pub struct App {
- pub tray_icon: Arc>>,
- pub exit: Arc,
- pub icon_manager: Arc,
-}
-
-impl App {
- pub fn new(icon_manager: IconManager, icon_name: &str, theme: Option) -> Result> {
- let (s, r) = std::sync::mpsc::channel::();
-
- let initial_icons = icon_manager.get_icon_set(icon_name, theme)
- .ok_or("Invalid initial icon name")?;
-
- let tray_icon = TrayIconBuilder::new()
- .sender(move |e: &Events| {
- let _ = s.send(e.clone());
- })
- .icon(initial_icons[0].clone())
- .tooltip("Nyan~")
- .menu(build_menu(&icon_manager))
- .on_double_click(Events::RunTaskmgr)
- .on_right_click(Events::ShowMenu)
- .build()?;
-
- let exit = Arc::new(AtomicBool::new(false));
- let tray_icon = Arc::new(Mutex::new(tray_icon));
- let icon_manager = Arc::new(icon_manager);
-
- let app = App {
- tray_icon,
- exit,
- icon_manager,
- };
-
- app.start_event_thread(r);
-
- Ok(app)
- }
-
- pub fn start_event_thread(&self, receiver: std::sync::mpsc::Receiver) {
- let exit = self.exit.clone();
- let tray_icon = self.tray_icon.clone();
- let icon_manager = self.icon_manager.clone();
-
- std::thread::spawn(move || {
- let update_menu = || {
- tray_icon
- .lock()
- .unwrap()
- .set_menu(&build_menu(&icon_manager))
- .expect("set_menu")
- };
-
- for event in receiver.iter() {
- match event {
- Events::Exit => {
- exit.store(true, Ordering::Relaxed);
- unsafe {
- PostQuitMessage(0);
- }
- break;
- }
- Events::RunTaskmgr => {
- if let Err(err) = safe_shell_execute("taskmgr.exe") {
- if let Err(msg_err) = safe_message_box(&err.to_string(), "RustCat Error", MB_OK.0) {
- eprintln!("Failed to show error dialog: {}", msg_err);
- }
- }
- },
- Events::SetTheme(theme) => {
- set_current_theme(Some(theme));
- update_menu();
- },
- Events::SetIcon(icon_name) => {
- set_current_icon(&icon_name);
- update_menu();
- },
- Events::ToggleRunOnStart => {
- let current_state = is_run_on_start_enabled();
- set_run_on_start(!current_state);
- update_menu();
- }
- Events::ShowAboutDialog => {
- let version = env!("CARGO_PKG_VERSION");
- let git_hash = option_env!("GIT_HASH").unwrap_or("N/A");
- let project_page = "https://github.com/bearice/RustCat";
- let message = format!(
- "RustCat version {} (Git: {})\nProject Page: {}",
- version, git_hash, project_page
- );
- if let Err(err) = safe_message_box(&message, "About RustCat", (MB_OK | MB_ICONINFORMATION).0) {
- eprintln!("Failed to show about dialog: {}", err);
- }
- },
- Events::ShowMenu => {
- tray_icon.lock().unwrap().show_menu().unwrap();
- },
- }
- }
- });
- }
-
- pub fn start_animation_thread(&self) {
- let exit = self.exit.clone();
- let tray_icon = self.tray_icon.clone();
- let icon_manager = self.icon_manager.clone();
-
- std::thread::spawn(move || {
- let sleep_interval = 10;
- let mut t1 = match cpu_usage::get_cpu_totals() {
- Ok(totals) => totals,
- Err(e) => {
- eprintln!("Failed to get initial CPU totals: {}", e);
- return;
- }
- };
- let mut update_counter = 0;
- let mut animate_counter = 0;
- let mut icon_index = 0;
- let mut speed = 200;
-
- while !exit.load(Ordering::Relaxed) {
- sleep(Duration::from_millis(sleep_interval));
- let current_icon_name = get_current_icon();
- let current_theme = get_current_theme();
- let icons = match icon_manager.get_icon_set(¤t_icon_name, Some(current_theme)) {
- Some(icons) => icons,
- None => {
- eprintln!("Invalid icon name: {}", current_icon_name);
- continue;
- }
- };
-
- if animate_counter >= speed {
- animate_counter = 0;
- icon_index += 1;
- icon_index %= icons.len();
- tray_icon
- .lock()
- .unwrap()
- .set_icon(&icons[icon_index])
- .map_err(|e| eprintln!("set_icon error: {:?}", e))
- .unwrap_or(());
- }
- animate_counter += sleep_interval;
-
- if update_counter == 1000 {
- update_counter = 0;
- let t2 = match cpu_usage::get_cpu_totals() {
- Ok(totals) => totals,
- Err(e) => {
- eprintln!("Failed to get CPU totals: {}", e);
- continue;
- }
- };
- let usage = 100.0 - (t2.1 - t1.1) / (t2.0 - t1.0) * 100.0;
- t1 = t2;
- speed = (200.0 / (usage / 5.0).clamp(1.0, 20.0)).round() as u64;
- println!("CPU Usage: {:.2}% speed: {}", usage, speed);
- tray_icon
- .lock()
- .unwrap()
- .set_tooltip(&format!("CPU Usage: {:.2}%", usage))
- .map_err(|e| eprintln!("set_tooltip error: {:?}", e))
- .unwrap_or(());
- }
- update_counter += sleep_interval;
- }
- });
- }
-
- pub fn run_message_loop(&self) {
- while !self.exit.load(Ordering::Relaxed) {
- match safe_message_loop() {
- Ok(()) => continue,
- Err(err) => {
- if err.to_string().contains("WM_QUIT") {
- break;
- } else {
- eprintln!("Message loop error: {}", err);
- break;
- }
- }
- }
- }
- }
-
- pub fn shutdown(&self) {
- self.exit.store(true, Ordering::Relaxed);
- }
-}
\ No newline at end of file
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::mpsc;
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::Duration;
+
+use crate::events::{build_menu, Events};
+use crate::icon_manager::{IconManager, Theme};
+use crate::platform::{CpuMonitor, SettingsManager, SystemIntegration};
+use crate::platform::{CpuMonitorImpl, SettingsManagerImpl, SystemIntegrationImpl};
+
+use trayicon::*;
+
+pub struct App {
+ tray_icon: Arc>>,
+ icon_manager: Arc,
+ exit_flag: Arc,
+ event_receiver: Option>,
+}
+
+impl App {
+ pub fn new(
+ icon_manager: IconManager,
+ initial_icon: &str,
+ initial_theme: Option,
+ ) -> Result> {
+ let (sender, receiver) = mpsc::channel::();
+ let icon_manager = Arc::new(icon_manager);
+ let exit_flag = Arc::new(AtomicBool::new(false));
+
+ let theme = initial_theme.unwrap_or_else(SettingsManagerImpl::get_current_theme);
+ let initial_icons = icon_manager
+ .get_icon_set(initial_icon, Some(theme))
+ .ok_or("Invalid initial icon name")?;
+
+ let tray_icon = TrayIconBuilder::new()
+ .sender(move |e: &Events| {
+ let _ = sender.send(e.clone());
+ })
+ .icon(initial_icons[0].clone())
+ .tooltip("~Nyan~ RustCat - CPU Usage Monitor")
+ .menu(build_menu(&icon_manager))
+ .on_right_click(Events::ShowMenu)
+ .on_double_click(Events::RunTaskmgr)
+ .build()?;
+
+ Ok(App {
+ tray_icon: Arc::new(Mutex::new(tray_icon)),
+ icon_manager,
+ exit_flag,
+ event_receiver: Some(receiver),
+ })
+ }
+
+ pub fn start_animation_thread(&self) {
+ let exit_flag = self.exit_flag.clone();
+ let tray_icon = self.tray_icon.clone();
+ let icon_manager = self.icon_manager.clone();
+
+ thread::spawn(move || {
+ let sleep_interval = 10;
+ let mut update_counter = 0;
+ let mut animate_counter = 0;
+ let mut icon_index = 0;
+ let mut speed = 200;
+
+ while !exit_flag.load(Ordering::Relaxed) {
+ thread::sleep(Duration::from_millis(sleep_interval));
+
+ let current_icon_name = SettingsManagerImpl::get_current_icon();
+ let current_theme = SettingsManagerImpl::get_current_theme();
+ let icons = match icon_manager.get_icon_set(¤t_icon_name, Some(current_theme))
+ {
+ Some(icons) => icons,
+ None => {
+ eprintln!("Invalid icon name: {}", current_icon_name);
+ continue;
+ }
+ };
+
+ if animate_counter >= speed {
+ animate_counter = 0;
+ icon_index += 1;
+ icon_index %= icons.len();
+ if let Ok(mut tray) = tray_icon.lock() {
+ if let Err(e) = tray.set_icon(&icons[icon_index]) {
+ eprintln!("set_icon error: {:?}", e);
+ }
+ }
+ }
+ animate_counter += sleep_interval;
+
+ if update_counter >= 1000 {
+ update_counter = 0;
+ let usage = match CpuMonitorImpl::get_cpu_usage() {
+ Ok(usage) => usage,
+ Err(e) => {
+ eprintln!("Failed to get CPU usage: {}", e);
+ continue;
+ }
+ };
+ speed = (200.0 / (usage / 5.0).clamp(1.0_f64, 20.0_f64)).round() as u64;
+ println!("CPU Usage: {:.2}% speed: {}", usage, speed);
+
+ if let Ok(mut tray) = tray_icon.lock() {
+ if let Err(e) = tray.set_tooltip(&format!("CPU Usage: {:.2}%", usage)) {
+ eprintln!("set_tooltip error: {:?}", e);
+ }
+ }
+ }
+ update_counter += sleep_interval;
+ }
+ });
+ }
+
+ pub fn run(mut self) {
+ if let Some(receiver) = self.event_receiver.take() {
+ for event in receiver {
+ match event {
+ Events::Exit => {
+ self.exit_flag.store(true, Ordering::Relaxed);
+ self.shutdown();
+ // For macOS, we need to trigger NSApplication termination
+ #[cfg(target_os = "macos")]
+ {
+ use objc2_app_kit::NSApplication;
+ use objc2_foundation::MainThreadMarker;
+ let app = NSApplication::sharedApplication(unsafe {
+ MainThreadMarker::new_unchecked()
+ });
+ unsafe { app.terminate(None) };
+ }
+ break;
+ }
+ Events::RunTaskmgr => {
+ if let Err(e) = SystemIntegrationImpl::open_system_monitor() {
+ eprintln!("Failed to open system monitor: {}", e);
+ }
+ }
+ Events::SetTheme(theme) => {
+ SettingsManagerImpl::set_current_theme(Some(theme));
+ self.update_menu();
+ }
+ Events::SetIcon(icon_name) => {
+ SettingsManagerImpl::set_current_icon(&icon_name);
+ self.update_menu();
+ }
+ Events::ToggleRunOnStart => {
+ let current_state = SettingsManagerImpl::is_run_on_start_enabled();
+ SettingsManagerImpl::set_run_on_start(!current_state);
+ self.update_menu();
+ }
+ Events::ShowAboutDialog => {
+ let version = env!("CARGO_PKG_VERSION");
+ let git_hash = option_env!("GIT_HASH").unwrap_or("N/A");
+ let project_page = "https://github.com/bearice/RustCat";
+ let message = format!(
+ "RustCat version {} (Git: {})\\nProject Page: {}",
+ version, git_hash, project_page
+ );
+
+ if let Err(e) =
+ SystemIntegrationImpl::show_dialog(&message, "About RustCat")
+ {
+ eprintln!("Failed to show about dialog: {}", e);
+ }
+ }
+ Events::ShowMenu => {
+ if let Ok(mut tray) = self.tray_icon.lock() {
+ if let Err(e) = tray.show_menu() {
+ eprintln!("Failed to show menu: {}", e);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn update_menu(&self) {
+ if let Ok(mut tray) = self.tray_icon.lock() {
+ if let Err(e) = tray.set_menu(&build_menu(&self.icon_manager)) {
+ eprintln!("Failed to update menu: {}", e);
+ }
+ }
+ }
+
+ pub fn shutdown(&self) {
+ // Platform-specific shutdown logic can be added here
+ println!("Shutting down RustCat...");
+ }
+}
diff --git a/src/cpu_usage.rs b/src/cpu_usage.rs
deleted file mode 100644
index 6566f41..0000000
--- a/src/cpu_usage.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use std::io;
-
-use windows::{
- Win32::Foundation::FILETIME,
- Win32::System::Threading::GetSystemTimes,
-};
-
-pub fn get_cpu_totals() -> io::Result<(f64, f64)> {
- let mut idle_time = empty();
- let mut kernel_time = empty();
- let mut user_time = empty();
- unsafe {
- GetSystemTimes(Some(&mut idle_time), Some(&mut kernel_time), Some(&mut user_time))
- .map_err(|e| io::Error::other(format!("Failed to get system times: {}", e)))?;
- }
- let idle_time = filetime_to_u64(idle_time) as f64;
- let kernel_time = filetime_to_u64(kernel_time) as f64;
- let user_time = filetime_to_u64(user_time) as f64;
- let total_time = kernel_time + user_time;
- Ok((total_time, idle_time))
-}
-
-/// Essentailly a no-op
-#[inline(always)]
-fn filetime_to_u64(f: FILETIME) -> u64 {
- (f.dwHighDateTime as u64) << 32 | (f.dwLowDateTime as u64)
-}
-
-/// Empty FILETIME
-#[inline(always)]
-fn empty() -> FILETIME {
- FILETIME {
- dwLowDateTime: 0,
- dwHighDateTime: 0,
- }
-}
diff --git a/src/events.rs b/src/events.rs
index 0e7ce6b..b78f8ba 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -1,64 +1,70 @@
-use trayicon::MenuBuilder;
-use crate::settings::{is_run_on_start_enabled, get_current_icon, get_current_theme};
-use crate::icon_manager::{Theme, IconManager};
-
-#[derive(Clone, Eq, PartialEq, Debug)]
-pub enum Events {
- Exit,
- SetTheme(Theme),
- SetIcon(String),
- RunTaskmgr,
- ToggleRunOnStart,
- ShowAboutDialog,
- ShowMenu,
-}
-
-pub fn build_menu(icon_manager: &IconManager) -> MenuBuilder {
- let run_on_start_enabled = is_run_on_start_enabled();
- let current_icon = get_current_icon();
- let current_theme = get_current_theme();
-
- let mut menu = MenuBuilder::new();
-
- // Build theme submenu - only show if current icon supports themes
- if icon_manager.supports_themes(¤t_icon) {
- let available_themes = icon_manager.available_themes_for_icon(¤t_icon);
- if !available_themes.is_empty() {
- let mut theme_menu = MenuBuilder::new();
- for theme in available_themes {
- let theme_name = match theme {
- Theme::Dark => "&Dark",
- Theme::Light => "&Light",
- };
- let is_current = current_theme == theme;
- theme_menu = theme_menu.checkable(theme_name, is_current, Events::SetTheme(theme));
- }
- menu = menu.submenu("&Theme", theme_menu);
- }
- }
-
- // Build icon submenu - dynamically from available icons
- let available_icons = icon_manager.available_icons();
- if available_icons.len() > 1 {
- let mut icon_menu = MenuBuilder::new();
- for icon_name in available_icons {
- let is_current = current_icon == icon_name;
- // Capitalize first letter for display
- let display_name = format!("&{}", icon_name.chars().next().unwrap().to_uppercase().collect::() + &icon_name[1..]);
- icon_menu = icon_menu.checkable(&display_name, is_current, Events::SetIcon(icon_name));
- }
- menu = menu.submenu("&Icon", icon_menu);
- }
-
- menu
- .separator()
- .checkable(
- "&Run on Start",
- run_on_start_enabled,
- Events::ToggleRunOnStart,
- )
- .separator()
- .item("&About", Events::ShowAboutDialog)
- .separator()
- .item("E&xit", Events::Exit)
-}
\ No newline at end of file
+use crate::icon_manager::{IconManager, Theme};
+use crate::platform::{SettingsManager, SettingsManagerImpl};
+use trayicon::MenuBuilder;
+
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub enum Events {
+ Exit,
+ SetTheme(Theme),
+ SetIcon(String),
+ RunTaskmgr,
+ ToggleRunOnStart,
+ ShowAboutDialog,
+ ShowMenu,
+}
+
+pub fn build_menu(icon_manager: &IconManager) -> MenuBuilder {
+ let run_on_start_enabled = SettingsManagerImpl::is_run_on_start_enabled();
+ let current_icon = SettingsManagerImpl::get_current_icon();
+ let current_theme = SettingsManagerImpl::get_current_theme();
+
+ let mut menu = MenuBuilder::new();
+
+ // Build theme submenu - only show if current icon supports themes
+ if icon_manager.supports_themes(¤t_icon) {
+ let available_themes = icon_manager.available_themes_for_icon(¤t_icon);
+ if !available_themes.is_empty() {
+ let mut theme_menu = MenuBuilder::new();
+ for theme in available_themes {
+ let theme_name = match theme {
+ Theme::Dark => "Dark",
+ Theme::Light => "Light",
+ };
+ let is_current = current_theme == theme;
+ theme_menu = theme_menu.checkable(theme_name, is_current, Events::SetTheme(theme));
+ }
+ menu = menu.submenu("Theme", theme_menu);
+ }
+ }
+
+ // Build icon submenu - dynamically from available icons
+ let available_icons = icon_manager.available_icons();
+ if available_icons.len() > 1 {
+ let mut icon_menu = MenuBuilder::new();
+ for icon_name in available_icons {
+ let is_current = current_icon == icon_name;
+ // Capitalize first letter for display
+ let display_name = icon_name
+ .chars()
+ .next()
+ .unwrap()
+ .to_uppercase()
+ .collect::()
+ + &icon_name[1..];
+ icon_menu = icon_menu.checkable(&display_name, is_current, Events::SetIcon(icon_name));
+ }
+ menu = menu.submenu("Icon", icon_menu);
+ }
+
+ menu.separator()
+ .checkable(
+ "Run on Start",
+ run_on_start_enabled,
+ Events::ToggleRunOnStart,
+ )
+ .separator()
+ .item("System Monitor", Events::RunTaskmgr)
+ .item("About", Events::ShowAboutDialog)
+ .separator()
+ .item("Exit", Events::Exit)
+}
diff --git a/src/icon_manager.rs b/src/icon_manager.rs
index 8737a90..140a9a5 100644
--- a/src/icon_manager.rs
+++ b/src/icon_manager.rs
@@ -1,174 +1,205 @@
-use trayicon::Icon;
-use std::collections::HashMap;
-use flate2::read::GzDecoder;
-use std::io::Read;
-
-#[allow(dead_code)]
-mod icon_data {
- pub struct IconGroupInfo {
- pub offset: usize,
- pub sizes: &'static [u32],
- }
- pub type IconData = HashMap<&'static str, HashMap<&'static str, IconGroupInfo>>;
- include!(concat!(env!("OUT_DIR"), "/icon_data.rs"));
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum Theme {
- Dark,
- Light,
-}
-
-impl Theme {
- pub fn from_system() -> Self {
- if crate::settings::is_dark_mode_enabled() {
- Theme::Dark
- } else {
- Theme::Light
- }
- }
-}
-
-impl std::fmt::Display for Theme {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Theme::Dark => write!(f, "dark"),
- Theme::Light => write!(f, "light"),
- }
- }
-}
-
-pub struct IconManager {
- // Maps base icon name -> theme -> icon frames
- icon_sets: HashMap>>,
- // Maps base icon name -> whether it supports themes
- theme_support: HashMap,
-}
-
-impl IconManager {
- pub fn new() -> Self {
- Self {
- icon_sets: HashMap::new(),
- theme_support: HashMap::new(),
- }
- }
-
- pub fn load_icons() -> Result> {
- let mut manager = Self::new();
-
- // Decompress the single big chunk containing all icons
- let mut decoder = GzDecoder::new(icon_data::ALL_ICONS_COMPRESSED);
- let mut all_decompressed = Vec::new();
- decoder.read_to_end(&mut all_decompressed)
- .map_err(|e| format!("Failed to decompress all icons: {}", e))?;
-
- // Leak the decompressed data to keep it alive for the lifetime of the program
- let all_decompressed = Box::leak(all_decompressed.into_boxed_slice());
-
- // Get icon metadata from build script generated module
- let icon_metadata_map = icon_data::get_icon_metadata();
-
- for (icon_name, theme_data) in icon_metadata_map {
- // All current icons support themes
- manager.theme_support.insert(icon_name.to_string(), true);
- let mut themes_map = HashMap::new();
-
- for (theme_str, group_info) in theme_data {
- let theme = match theme_str {
- "dark" => Theme::Dark,
- "light" => Theme::Light,
- _ => continue, // Skip unknown themes
- };
-
- // Extract this group's data from the big decompressed chunk
- let mut icons = Vec::new();
- let mut current_offset = group_info.offset;
-
- for &size in group_info.sizes {
- let size = size as usize;
- if current_offset + size > all_decompressed.len() {
- return Err(format!("Invalid icon offset/size data for {} {}", icon_name, theme_str).into());
- }
-
- let icon_data = &all_decompressed[current_offset..current_offset + size];
-
- let icon = Icon::from_buffer(icon_data, None, None)
- .map_err(|e| format!("Failed to create icon from buffer for {} {}: {}", icon_name, theme_str, e))?;
-
- icons.push(icon);
- current_offset += size;
- }
-
- themes_map.insert(theme, icons);
- }
-
- manager.icon_sets.insert(icon_name.to_string(), themes_map);
- }
-
- Ok(manager)
- }
-
- pub fn get_icon_set(&self, icon_name: &str, theme: Option) -> Option<&Vec> {
- let icon_map = self.icon_sets.get(icon_name)?;
-
- if let Some(theme) = theme {
- // Specific theme requested
- icon_map.get(&theme)
- } else {
- // Auto-detect theme or use first available
- if self.supports_themes(icon_name) {
- let system_theme = Theme::from_system();
- icon_map.get(&system_theme)
- .or_else(|| icon_map.values().next()) // Fallback to any theme
- } else {
- icon_map.values().next() // Single theme icon
- }
- }
- }
-
- pub fn supports_themes(&self, icon_name: &str) -> bool {
- self.theme_support.get(icon_name).copied().unwrap_or(false)
- }
-
- pub fn available_icons(&self) -> Vec {
- let mut icons: Vec = self.icon_sets.keys().cloned().collect();
- icons.sort();
- icons
- }
-
- pub fn available_themes_for_icon(&self, icon_name: &str) -> Vec {
- if let Some(icon_map) = self.icon_sets.get(icon_name) {
- let mut themes: Vec = icon_map.keys().copied().collect();
- themes.sort_by_key(|t| match t {
- Theme::Dark => 0,
- Theme::Light => 1,
- });
- themes
- } else {
- vec![]
- }
- }
-
- // Migration support - convert old numeric IDs to string IDs
- pub fn migrate_from_numeric_id(old_id: usize) -> String {
- let is_cat = (old_id & 2) == 0;
-
- if is_cat {
- "cat".to_string()
- } else {
- "parrot".to_string()
- }
- }
-
- pub fn get_theme_from_numeric_id(old_id: usize) -> Theme {
- let is_dark = (old_id & 1) == 0;
- if is_dark { Theme::Dark } else { Theme::Light }
- }
-
-}
-
-impl Default for IconManager {
- fn default() -> Self {
- Self::new()
- }
-}
\ No newline at end of file
+use flate2::read::GzDecoder;
+use std::collections::HashMap;
+use std::io::Read;
+use trayicon::Icon;
+
+#[allow(dead_code)]
+mod icon_data {
+ pub struct IconGroupInfo {
+ pub offset: usize,
+ pub sizes: &'static [u32],
+ }
+ pub type IconData = HashMap<&'static str, HashMap<&'static str, IconGroupInfo>>;
+ include!(concat!(env!("OUT_DIR"), "/icon_data.rs"));
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Theme {
+ Dark,
+ Light,
+}
+
+impl Theme {
+ pub fn from_system() -> Self {
+ use crate::platform::{SettingsManager, SettingsManagerImpl};
+ if SettingsManagerImpl::is_dark_mode_enabled() {
+ Theme::Dark
+ } else {
+ Theme::Light
+ }
+ }
+}
+
+impl std::fmt::Display for Theme {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Theme::Dark => write!(f, "dark"),
+ Theme::Light => write!(f, "light"),
+ }
+ }
+}
+
+pub struct IconManager {
+ // Maps base icon name -> theme -> icon frames
+ icon_sets: HashMap>>,
+ // Maps base icon name -> whether it supports themes
+ theme_support: HashMap,
+}
+
+impl IconManager {
+ pub fn new() -> Self {
+ Self {
+ icon_sets: HashMap::new(),
+ theme_support: HashMap::new(),
+ }
+ }
+
+ pub fn load_icons() -> Result> {
+ let mut manager = Self::new();
+
+ // Decompress the single big chunk containing all icons
+ let mut decoder = GzDecoder::new(icon_data::ALL_ICONS_COMPRESSED);
+ let mut all_decompressed = Vec::new();
+ decoder
+ .read_to_end(&mut all_decompressed)
+ .map_err(|e| format!("Failed to decompress all icons: {}", e))?;
+
+ // Leak the decompressed data to keep it alive for the lifetime of the program
+ let all_decompressed = Box::leak(all_decompressed.into_boxed_slice());
+
+ // Get icon metadata from build script generated module
+ let icon_metadata_map = icon_data::get_icon_metadata();
+
+ for (icon_name, theme_data) in icon_metadata_map {
+ // All current icons support themes
+ manager.theme_support.insert(icon_name.to_string(), true);
+ let mut themes_map = HashMap::new();
+
+ for (theme_str, group_info) in theme_data {
+ let theme = match theme_str {
+ "dark" => Theme::Dark,
+ "light" => Theme::Light,
+ _ => continue, // Skip unknown themes
+ };
+
+ // Extract this group's data from the big decompressed chunk
+ let mut icons = Vec::new();
+ let mut current_offset = group_info.offset;
+
+ for &size in group_info.sizes {
+ let size = size as usize;
+ if current_offset + size > all_decompressed.len() {
+ return Err(format!(
+ "Invalid icon offset/size data for {} {}",
+ icon_name, theme_str
+ )
+ .into());
+ }
+
+ let icon_data = &all_decompressed[current_offset..current_offset + size];
+
+ let icon = {
+ #[cfg(windows)]
+ {
+ Icon::from_buffer(icon_data, None, None).map_err(|e| {
+ format!(
+ "Failed to create icon from buffer for {} {}: {}",
+ icon_name, theme_str, e
+ )
+ })?
+ }
+ #[cfg(target_os = "macos")]
+ {
+ // macOS tray icons should be smaller (16x16 is the standard size)
+ Icon::from_buffer(icon_data, Some(16), Some(16)).map_err(|e| {
+ format!(
+ "Failed to create icon from buffer for {} {}: {}",
+ icon_name, theme_str, e
+ )
+ })?
+ }
+ };
+
+ icons.push(icon);
+ current_offset += size;
+ }
+
+ themes_map.insert(theme, icons);
+ }
+
+ manager.icon_sets.insert(icon_name.to_string(), themes_map);
+ }
+
+ Ok(manager)
+ }
+
+ pub fn get_icon_set(&self, icon_name: &str, theme: Option) -> Option<&Vec> {
+ let icon_map = self.icon_sets.get(icon_name)?;
+
+ if let Some(theme) = theme {
+ // Specific theme requested
+ icon_map.get(&theme)
+ } else {
+ // Auto-detect theme or use first available
+ if self.supports_themes(icon_name) {
+ let system_theme = Theme::from_system();
+ icon_map
+ .get(&system_theme)
+ .or_else(|| icon_map.values().next()) // Fallback to any theme
+ } else {
+ icon_map.values().next() // Single theme icon
+ }
+ }
+ }
+
+ pub fn supports_themes(&self, icon_name: &str) -> bool {
+ self.theme_support.get(icon_name).copied().unwrap_or(false)
+ }
+
+ pub fn available_icons(&self) -> Vec {
+ let mut icons: Vec = self.icon_sets.keys().cloned().collect();
+ icons.sort();
+ icons
+ }
+
+ pub fn available_themes_for_icon(&self, icon_name: &str) -> Vec {
+ if let Some(icon_map) = self.icon_sets.get(icon_name) {
+ let mut themes: Vec = icon_map.keys().copied().collect();
+ themes.sort_by_key(|t| match t {
+ Theme::Dark => 0,
+ Theme::Light => 1,
+ });
+ themes
+ } else {
+ vec![]
+ }
+ }
+
+ // Migration support - convert old numeric IDs to string IDs
+ #[allow(dead_code)]
+ pub fn migrate_from_numeric_id(old_id: usize) -> String {
+ let is_cat = (old_id & 2) == 0;
+
+ if is_cat {
+ "cat".to_string()
+ } else {
+ "parrot".to_string()
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_theme_from_numeric_id(old_id: usize) -> Theme {
+ let is_dark = (old_id & 1) == 0;
+ if is_dark {
+ Theme::Dark
+ } else {
+ Theme::Light
+ }
+ }
+}
+
+impl Default for IconManager {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 433631d..4150bd2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,44 +1,44 @@
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-
-use windows::Win32::UI::WindowsAndMessaging::MB_OK;
-
-mod app;
-mod cpu_usage;
-mod events;
-mod icon_manager;
-mod settings;
-mod windows_api;
-
-use crate::{
- app::App,
- icon_manager::IconManager,
- settings::{get_current_icon, get_current_theme, migrate_legacy_settings},
- windows_api::safe_message_box,
-};
-
-fn main() {
- // Migrate legacy settings if needed
- migrate_legacy_settings();
-
- // Load icons
- let icon_manager = IconManager::load_icons().expect("Failed to load icons");
-
- // Get current icon and theme
- let icon_name = get_current_icon();
- let theme = get_current_theme();
-
- std::panic::set_hook(Box::new(|e| {
- let msg = format!("Panic: {}", e);
- if let Err(err) = safe_message_box(&msg, "RustCat Error", MB_OK.0) {
- eprintln!("Failed to show panic dialog: {}", err);
- }
- }));
-
- let app = App::new(icon_manager, &icon_name, Some(theme)).expect("Failed to create app");
-
- app.start_animation_thread();
-
- app.run_message_loop();
-
- app.shutdown();
-}
+#![cfg_attr(all(not(debug_assertions), windows), windows_subsystem = "windows")]
+
+mod app;
+mod events;
+mod icon_manager;
+mod platform;
+
+use crate::{
+ icon_manager::IconManager,
+ platform::{SettingsManager, SettingsManagerImpl, SystemIntegration, SystemIntegrationImpl},
+};
+
+#[cfg(target_os = "macos")]
+use crate::platform::macos::app::MacosApp;
+#[cfg(windows)]
+use crate::platform::windows::app::WindowsApp;
+
+fn main() {
+ // Migrate legacy settings if needed
+ SettingsManagerImpl::migrate_legacy_settings();
+
+ // Load icons
+ let icon_manager = IconManager::load_icons().expect("Failed to load icons");
+
+ // Get current icon and theme
+ let icon_name = SettingsManagerImpl::get_current_icon();
+ let theme = SettingsManagerImpl::get_current_theme();
+
+ std::panic::set_hook(Box::new(|e| {
+ let msg = format!("Panic: {}", e);
+ if let Err(err) = SystemIntegrationImpl::show_dialog(&msg, "RustCat Error") {
+ eprintln!("Failed to show panic dialog: {}", err);
+ }
+ }));
+
+ #[cfg(target_os = "windows")]
+ let app = WindowsApp::new(icon_manager, &icon_name, Some(theme)).expect("Failed to create app");
+ #[cfg(target_os = "macos")]
+ let app = MacosApp::new(icon_manager, &icon_name, Some(theme)).expect("Failed to create app");
+
+ app.start_animation_thread();
+
+ app.run();
+}
diff --git a/src/platform/macos/app.rs b/src/platform/macos/app.rs
new file mode 100644
index 0000000..2f869cc
--- /dev/null
+++ b/src/platform/macos/app.rs
@@ -0,0 +1,46 @@
+use crate::app::App;
+use crate::icon_manager::{IconManager, Theme};
+use objc2::rc::Retained;
+use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};
+use objc2_foundation::MainThreadMarker;
+use std::sync::Arc;
+
+pub struct MacosApp {
+ app: Arc,
+ ns_app: Retained,
+}
+
+impl MacosApp {
+ pub fn new(
+ icon_manager: IconManager,
+ initial_icon: &str,
+ initial_theme: Option,
+ ) -> Result> {
+ // Initialize NSApplication for macOS
+ let mtm = unsafe { MainThreadMarker::new_unchecked() };
+ let ns_app = NSApplication::sharedApplication(mtm);
+ ns_app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
+
+ let app = Arc::new(App::new(icon_manager, initial_icon, initial_theme)?);
+ Ok(MacosApp { app, ns_app })
+ }
+
+ pub fn start_animation_thread(&self) {
+ self.app.start_animation_thread();
+ }
+
+ pub fn run(self) {
+ // Start the app event handler in a separate thread
+ let app = Arc::try_unwrap(self.app).unwrap_or_else(|_| {
+ panic!("Failed to unwrap Arc - multiple references exist");
+ });
+ std::thread::spawn(move || {
+ app.run();
+ });
+
+ // Run the macOS application main loop
+ unsafe {
+ self.ns_app.run();
+ }
+ }
+}
diff --git a/src/platform/macos/cpu_usage.rs b/src/platform/macos/cpu_usage.rs
new file mode 100644
index 0000000..1c02c77
--- /dev/null
+++ b/src/platform/macos/cpu_usage.rs
@@ -0,0 +1,78 @@
+use crate::platform::CpuMonitor;
+use std::io;
+use std::sync::Mutex;
+
+pub struct MacosCpuMonitor;
+
+static CPU_STATE: Mutex