This commit is contained in:
2026-04-30 19:44:14 +02:00
commit a944768636
9 changed files with 1787 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+543
View File
@@ -0,0 +1,543 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cocoa"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation",
"core-graphics-types",
"libc",
"objc",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"libc",
]
[[package]]
name = "ctrlc"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"dispatch2",
"nix",
"windows-sys 0.61.2",
]
[[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 = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.11.0",
"block2",
"libc",
"objc2",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "libc"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "libredox"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "nix"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[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.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
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 = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"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.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[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_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[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_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[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_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "zen-widget"
version = "0.1.0"
dependencies = [
"cocoa",
"core-graphics",
"ctrlc",
"dirs",
"objc",
"serde",
"toml",
]
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "zen-widget"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
dirs = "5"
ctrlc = "3"
# Cocoa/AppKit for floating window
cocoa = "0.25"
core-graphics = "0.23"
# For cross-platform event handling
objc = "0.2"
+133
View File
@@ -0,0 +1,133 @@
# Zen Widget
A floating ASCII companion that watches your terminal and comments on your work. Dead simple to install.
## Current Status
**Working:** Terminal-based demo that displays the widget in your terminal.
**Planned:** Floating window version that stays on top of your terminal.
## Installation
```bash
# 1. Clone
git clone https://github.com/yourname/zen-widget.git ~/zen-widget
cd ~/zen-widget
# 2. Build (requires Rust)
cargo build --release
# 3. Run
./target/release/zen-widget
```
## Requirements
- macOS (window detection uses CGWindowList APIs)
- tmux (for terminal content analysis)
- Rust (for building)
## Quick Demo
```bash
cargo run
```
This displays:
```
╔══════════════════════════════════════════╗
║ Zen Widget Starting ║
╚══════════════════════════════════════════╝
Terminals: ["Ghostty", "iTerm2", "Alacritty", ...]
Corner: top-right
Creature: ghost
Tmux Session: zen
Size: 200x150
Press Ctrl+C to exit
```
## Architecture
```
┌──────────────────────────────────────┐
│ zen-widget (Rust) │
│ - Monitors tmux session │
│ - Displays ASCII creature │
│ - Generates contextual quips │
└──────────────────────────────────────┘
│ capture-pane -p
┌──────────────────────────────────────┐
│ tmux session "zen" │
│ (hidden, for communication) │
└──────────────────────────────────────┘
```
## Configuration
Edit `~/.config/zen-widget/config.toml`:
```toml
# Terminals to track
target_terminals = ["Ghostty", "iTerm2", "Alacritty", "Terminal", "Kitty", "WezTerm"]
# Corner: top-left, top-right, bottom-left, bottom-right
corner = "top-right"
# Creature type: ghost, cat, octopus, bean, slime
creature = "ghost"
# tmux session name
tmux_session = "zen"
# Widget size
width = 200
height = 150
# Opacity (0.0 to 1.0)
opacity = 0.85
# Comment update interval (ticks, ~1 tick per second)
comment_interval = 30
# Position refresh interval (ms)
refresh_ms = 10000
```
## How It Works
### 1. tmux Bridge
- Creates hidden tmux session named `zen`
- Widget captures pane content: `tmux capture-pane -t zen -p`
- Parses output, generates appropriate quip
### 2. Creature Animation
- 5 creature types: ghost, cat, octopus, bean, slime
- Simple walking animation (3 frames)
- Random quips based on detected activity
### 3. Context-Aware Quips
- **Errors**: "oof, that doesn't look great"
- **Warnings**: "just a warning, probably fine"
- **Builds**: "compiling, make a coffee"
- **Git**: "making commits, good human"
- **Cargo/npm**: "downloading the internet"
## Future: Floating Window
The plan is to add a floating widget window using standard macOS APIs:
- `NSWindow` with `level = .floating`
- Position tracking via `CGWindowListCopyWindowInfo`
- Semi-transparent background
- ASCII creature + quip display
This avoids yabai/SIP complexity but requires Accessibility permissions.
## License
MIT
+133
View File
@@ -0,0 +1,133 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
/// Terminal apps to track
#[serde(default = "default_terminals")]
pub target_terminals: Vec<String>,
/// Corner to position widget: "top-left", "top-right", "bottom-left", "bottom-right"
#[serde(default = "default_corner")]
pub corner: String,
/// Creature type: "ghost", "cat", "octopus", "bean", "slime"
#[serde(default = "default_creature")]
pub creature: String,
/// Tmux session name for communication
#[serde(default = "default_tmux_session")]
pub tmux_session: String,
/// Widget window width
#[serde(default = "default_width")]
pub width: u32,
/// Widget window height
#[serde(default = "default_height")]
pub height: u32,
/// Widget opacity (0.0 to 1.0)
#[serde(default = "default_opacity")]
pub opacity: f64,
/// How often to update comment (in ticks, ~1 tick per second)
#[serde(default = "default_comment_interval")]
pub comment_interval: u64,
/// Refresh position interval in ms
#[serde(default = "default_refresh_ms")]
pub refresh_ms: u64,
}
fn default_terminals() -> Vec<String> {
vec![
"Ghostty".to_string(),
"iTerm2".to_string(),
"Alacritty".to_string(),
"Terminal".to_string(),
"Kitty".to_string(),
"WezTerm".to_string(),
]
}
fn default_corner() -> String {
"top-right".to_string()
}
fn default_creature() -> String {
"ghost".to_string()
}
fn default_tmux_session() -> String {
"zen".to_string()
}
fn default_width() -> u32 {
300
}
fn default_height() -> u32 {
200
}
fn default_opacity() -> f64 {
0.85
}
fn default_comment_interval() -> u64 {
30
}
fn default_refresh_ms() -> u64 {
10000
}
impl Default for Config {
fn default() -> Self {
Self {
target_terminals: default_terminals(),
corner: default_corner(),
creature: default_creature(),
tmux_session: default_tmux_session(),
width: default_width(),
height: default_height(),
opacity: default_opacity(),
comment_interval: default_comment_interval(),
refresh_ms: default_refresh_ms(),
}
}
}
impl Config {
pub fn load() -> Self {
let config_path = Self::config_path();
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)
.expect("Failed to read config file");
toml::from_str(&content).unwrap_or_else(|e| {
eprintln!("Failed to parse config: {}, using defaults", e);
Config::default()
})
} else {
let default_config = Config::default();
// Create config directory and file
if let Some(dir) = config_path.parent() {
std::fs::create_dir_all(dir).ok();
}
if let Ok(content) = toml::to_string_pretty(&default_config) {
std::fs::write(&config_path, &content).ok();
println!("Created default config at {:?}", config_path);
}
default_config
}
}
fn config_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("zen-widget")
.join("config.toml")
}
}
+217
View File
@@ -0,0 +1,217 @@
use std::time::{SystemTime, UNIX_EPOCH};
// ASCII art frames for different creatures
const GHOST_FRAMES: &[&str] = &[
r#" /
/~
| |_
|___|"#,
r#" |
/~
/|
|_
|___|"#,
r#" \
/~
| |
|_|_
|___|"#,
r#" |
\~|
\|
_|_
|___|"#,
];
const CAT_FRAMES: &[&str] = &[
r#" /\__/\
/ \
:| o o |
:| < |
\_____/"#,
r#" /\__/\
/ \
:| o o |
:| ^ |
\_____/"#,
r#" /\__/\
/ \
:| - - |
:| < |
\_____/"#,
];
const OCTOPUS_FRAMES: &[&str] = &[
r#" ____
/ \
| oo |
\____/
||||
/\/||\/\"#,
r#" ____
/ \
| oo |
\____/
||||
\/||\/\"#,
r#" ____
/ \
| oo |
\____/
||||
/\||/\ "#,
r#" ____
/ \
| oo |
\____/
||||
\/||\/\"#,
];
const BEAN_FRAMES: &[&str] = &[
r#" ___
/o o\
\___/"#,
r#" ___
/- -\
\___/"#,
r#" ___
\_o_/
/ \"#,
r#" ___
\_-\_/
/ \"#,
];
const SLIME_FRAMES: &[&str] = &[
r#" ___
/ \
| |
\___/"#,
r#" _____
/ \
:| o o |
\_____/"#,
r#" _____
/ \
:| o |
\_____/"#,
r#" ___
/ \
| oo |
\___/"#,
];
pub fn get_frames(creature: &str) -> &'static [&'static str] {
match creature.to_lowercase().as_str() {
"ghost" => &GHOST_FRAMES,
"cat" => &CAT_FRAMES,
"octopus" => &OCTOPUS_FRAMES,
"bean" => &BEAN_FRAMES,
"slime" => &SLIME_FRAMES,
_ => &GHOST_FRAMES,
}
}
pub fn draw_creature(creature: &str, tick: u64) -> String {
let frames = get_frames(creature);
let frame_idx = (tick as usize / 3) % frames.len();
frames[frame_idx].to_string()
}
pub fn get_creature_state(tick: u64) -> String {
// Simple state: idle, walking, excited
match tick % 20 {
0..=10 => "idle".to_string(),
11..=15 => "walking".to_string(),
16..=18 => "excited".to_string(),
_ => "idle".to_string(),
}
}
pub fn get_comment(content: &str) -> String {
let content_lower = content.to_lowercase();
// Analyze tmux content and generate appropriate quip
if content_lower.contains("error") || content_lower.contains("failed") || content_lower.contains("panic") {
quip(&[
"oof, that doesn't look great",
"errors happen, keep going",
"the compiler is skeptical",
"that's a spicy meatball",
])
} else if content_lower.contains("warning") {
quip(&[
"just a warning, probably fine",
"the compiler has concerns",
"minor hiccup",
])
} else if content_lower.contains("compil") || content_lower.contains("build") {
quip(&[
"building... patience",
"compiling, make a coffee",
"the magic happens slowly",
])
} else if content_lower.contains("git") {
quip(&[
"making commits, good human",
"version control time",
"pushing to the cloud",
])
} else if content_lower.contains("cargo") || content_lower.contains("npm") || content_lower.contains("pip") {
quip(&[
"downloading the internet",
"dependencies incoming",
"a folder full of hope",
])
} else if content_lower.contains("rust") {
quip(&[
"may your borrow checker be satisfied",
"fearless concurrency vibes",
"rustacean at work",
])
} else if content_lower.is_empty() {
quip(&[
"thinking...",
"nothing happening",
"the void stares back",
])
} else {
quip(&[
"looking productive",
"the bits are flowing",
"i'm watching you work",
"nice typing",
"carrying on as usual",
])
}
}
fn quip(options: &[&str]) -> String {
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as usize;
options[seed % options.len()].to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_draw_creature() {
let ghost = draw_creature("ghost", 0);
assert!(ghost.contains('/') || ghost.contains('|'));
}
#[test]
fn test_get_comment() {
let error_comment = get_comment("error: something failed");
assert!(!error_comment.is_empty());
let empty_comment = get_comment("");
assert!(!empty_comment.is_empty());
}
}
+205
View File
@@ -0,0 +1,205 @@
#[macro_use]
extern crate objc;
mod config;
mod creature;
mod tmux;
mod window;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use crate::config::Config;
use crate::creature::{draw_creature, get_comment};
use crate::tmux::get_tmux_content;
use crate::window::{
create_floating_window, get_widget_position, Corner, StatusBarMenu,
};
static RUNNING: AtomicBool = AtomicBool::new(true);
fn main() {
// Parse config
let config = Config::load();
println!("╔══════════════════════════════════════════╗");
println!("║ Zen Widget Starting ║");
println!("╚══════════════════════════════════════════╝");
println!();
println!(" Terminals: {:?}", config.target_terminals);
println!(" Corner: {}", config.corner);
println!(" Creature: {}", config.creature);
println!(" Tmux Session: {}", config.tmux_session);
println!(" Size: {}x{}", config.width, config.height);
println!(" Opacity: {}", config.opacity);
println!();
// Set up Ctrl+C handler
ctrlc::set_handler(|| {
println!("\nShutting down Zen Widget...");
RUNNING.store(false, Ordering::SeqCst);
}).expect("Error setting Ctrl-C handler");
// Ensure tmux session exists
let tmux_content = get_tmux_content(&config.tmux_session);
if tmux_content.is_empty() {
println!(" [tmux] Created new session '{}'", config.tmux_session);
} else {
println!(" [tmux] Using existing session '{}'", config.tmux_session);
}
// Get initial widget position
let corner = Corner::from_str(&config.corner);
let (pos_x, pos_y) = match get_widget_position(&corner, config.width, config.height) {
Some((x, y)) => {
println!(" [window] Position: ({:.0}, {:.0})", x, y);
println!(" [window] Widget size: {}x{}", config.width, config.height);
// Print screen info
if let Some((sx, sy, sw, sh)) = crate::window::get_screen_info() {
println!(" [window] Screen: {}x{} at ({}, {})", sw, sh, sx, sy);
}
(x, y)
}
None => {
eprintln!(" [window] Could not get screen dimensions, using defaults");
(100.0, 100.0)
}
};
// Create floating window
println!(" [window] Creating floating window...");
let window = create_floating_window(
pos_x,
pos_y,
config.width as f64,
config.height as f64,
config.opacity,
);
if let Some(fw) = window {
println!(" [window] Floating window created!");
// Current creature state
let current_creature = config.creature.clone();
// Create status bar menu
println!(" [menu] Creating status bar menu...");
let mut status_menu = StatusBarMenu::new();
// Set up creature change callback
status_menu.setup_menu(move |new_creature| {
println!(" [menu] Creature changed to: {}", new_creature);
});
println!(" [menu] Status bar menu created!");
println!();
println!(" Press Ctrl+C to exit");
println!();
// Initial content
let tmux_content = get_tmux_content(&config.tmux_session);
let comment = get_comment(&tmux_content);
let content = format_widget(&config.creature, 0, &comment);
fw.update_content(&content);
println!(" Window ready! The widget should now be visible on your screen.");
println!(" Look for:");
println!(" 1. A dark gray box in the TOP-RIGHT corner");
println!(" 2. 'Zen' text in the menu bar");
println!();
println!(" Press Ctrl+C to exit");
println!();
// Run the Cocoa event loop
// This keeps the window alive and responsive
fw.run();
println!("\nZen Widget stopped.");
} else {
eprintln!("Failed to create floating window. Make sure you're running on macOS.");
eprintln!("Falling back to terminal-only mode...");
println!();
// Fallback to terminal-only mode
let mut tick = 0u64;
while RUNNING.load(Ordering::SeqCst) {
thread::sleep(Duration::from_secs(1));
tick += 1;
if tick % config.comment_interval == 0 {
let tmux_content = get_tmux_content(&config.tmux_session);
let comment = get_comment(&tmux_content);
print_widget(&config.creature, tick, &comment);
}
}
}
}
fn format_widget(creature: &str, tick: u64, comment: &str) -> String {
let creature_lines = draw_creature(creature, tick);
let max_line_width = creature_lines.lines()
.map(|l| l.len())
.max()
.unwrap_or(20);
let comment_width = comment.len() + 4;
let total_width = max_line_width.max(comment_width).max(15);
let border = "".repeat(total_width);
let mut result = format!("┌─{}─┐\n", border);
let title = " Zen Widget ";
let padding = total_width - title.len();
let left_pad = padding / 2;
let right_pad = padding - left_pad;
result.push_str(&format!("{}{}{}\n", " ".repeat(left_pad), title, " ".repeat(right_pad)));
for line in creature_lines.lines() {
let padding = total_width - line.len();
result.push_str(&format!("{}\n", line.to_string() + &" ".repeat(padding)));
}
result.push_str(&format!("{}\n", " ".repeat(total_width)));
// Comment with wrapping
let wrapped_comment = wrap_text(comment, total_width - 4);
for line in wrapped_comment {
let padding = total_width - line.len() - 2;
result.push_str(&format!("{}\n", " ".repeat(2) + &line + &" ".repeat(padding)));
}
result.push_str(&format!("└─{}─┘", border));
result
}
fn print_widget(creature: &str, tick: u64, comment: &str) {
let content = format_widget(creature, tick, comment);
println!("{}", content);
println!();
}
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.len() + word.len() + 1 <= max_width {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
} else {
if !current_line.is_empty() {
lines.push(current_line.clone());
}
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
+63
View File
@@ -0,0 +1,63 @@
use std::process::Command;
/// Get the content from a tmux pane
pub fn get_tmux_content(session_name: &str) -> String {
// First check if session exists
if !session_exists(session_name) {
// Create the session if it doesn't exist
let create_output = Command::new("tmux")
.args(&["new-session", "-d", "-s", session_name, "-x", "80", "-y", "24"])
.output();
if create_output.map(|o| !o.status.success()).unwrap_or(true) {
return String::new();
}
}
// Capture the pane content
let output = Command::new("tmux")
.args(&["capture-pane", "-t", session_name, "-p"])
.output();
match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
_ => String::new(),
}
}
/// Check if a tmux session exists
fn session_exists(session_name: &str) -> bool {
Command::new("tmux")
.args(&["has-session", "-t", session_name])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Send a message to a tmux session (useful for testing)
#[allow(dead_code)]
pub fn send_tmux_message(session_name: &str, message: &str) -> bool {
Command::new("tmux")
.args(&["send-keys", "-t", session_name, message, "Enter"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_tmux_content() {
// This will create the session if it doesn't exist
let content = get_tmux_content("zen-widget-test");
// Should return something (possibly empty)
assert!(content.len() >= 0);
// Clean up
let _ = Command::new("tmux")
.args(&["kill-session", "-t", "zen-widget-test"])
.output();
}
}
+475
View File
@@ -0,0 +1,475 @@
use std::sync::atomic::{AtomicBool, Ordering};
use core_graphics::display::CGDisplay;
use core_graphics::geometry::CGRect;
use cocoa::appkit::{NSMenu, NSWindow};
use cocoa::base::{nil, id, YES, NO};
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSDate};
use objc::msg_send;
use objc::sel;
// NSDefaultRunLoopMode constant
const NSDefaultRunLoopMode: &'static str = "NSDefaultRunLoopMode";
// Debug flag - defaults to true
static DEBUG_MODE: AtomicBool = AtomicBool::new(true);
#[macro_export]
macro_rules! dprintln {
($($arg:tt)*) => {
if $crate::window::is_debug_enabled() {
println!($($arg)*);
}
};
}
pub fn is_debug_enabled() -> bool {
DEBUG_MODE.load(Ordering::SeqCst)
}
pub fn set_debug_enabled(enabled: bool) {
DEBUG_MODE.store(enabled, Ordering::SeqCst);
println!("[DEBUG] Debug mode: {}", if enabled { "ON" } else { "OFF" });
}
#[derive(Debug, Clone, Copy)]
pub enum Corner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl Corner {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"top-left" | "topleft" => Corner::TopLeft,
"top-right" | "topright" => Corner::TopRight,
"bottom-left" | "bottomleft" => Corner::BottomLeft,
"bottom-right" | "bottomright" => Corner::BottomRight,
_ => Corner::TopRight,
}
}
pub fn position(&self, screen: &CGRect, widget_width: f64, widget_height: f64, offset: i32) -> (f64, f64) {
let x = match self {
Corner::TopLeft | Corner::BottomLeft => screen.origin.x + offset as f64,
Corner::TopRight | Corner::BottomRight => screen.origin.x + screen.size.width - widget_width - offset as f64,
};
// For y, CG displays use bottom-left origin, but NSWindow uses top-left origin
// So we need to flip: y_ns = screen_height - widget_y_cg - widget_height
let screen_height = screen.size.height;
let y_cg = match self {
Corner::TopLeft | Corner::TopRight => screen_height - widget_height - offset as f64,
Corner::BottomLeft | Corner::BottomRight => screen.origin.y + offset as f64,
};
let y = screen_height - y_cg - widget_height;
(x, y)
}
pub fn name(&self) -> &'static str {
match self {
Corner::TopLeft => "Top Left",
Corner::TopRight => "Top Right",
Corner::BottomLeft => "Bottom Left",
Corner::BottomRight => "Bottom Right",
}
}
}
#[derive(Debug)]
pub struct WindowInfo {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub owner_name: String,
}
fn get_main_display() -> Option<CGRect> {
let display = CGDisplay::main();
let bounds = display.bounds();
Some(bounds)
}
/// Get screen dimensions
pub fn get_screen_info() -> Option<(f64, f64, f64, f64)> {
let display = CGDisplay::main();
let bounds = display.bounds();
Some((bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height))
}
/// Find the position of a terminal window and calculate widget position
pub fn find_terminal_position(_target_terminals: &[String], corner_str: &str) -> Option<(i32, i32)> {
let corner = Corner::from_str(corner_str);
let screen = get_main_display()?;
let offset = 20;
let (x, y) = corner.position(&screen, 200.0, 150.0, offset);
Some((x as i32, y as i32))
}
// ============================================================================
// Floating Window Implementation
// ============================================================================
const NSFLOATING_WINDOW_LEVEL: i32 = 3;
// Global state for the Cocoa thread
static WINDOW_HANDLE: std::sync::atomic::AtomicPtr<std::ffi::c_void> = std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
static TEXT_FIELD_HANDLE: std::sync::atomic::AtomicPtr<std::ffi::c_void> = std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
pub struct FloatingWindow {
window: id,
text_field: id,
width: f64,
height: f64,
}
impl FloatingWindow {
pub fn new(x: f64, y: f64, width: f64, height: f64, opacity: f64) -> Self {
unsafe {
// Initialize NSApplication
let ns_app: id = msg_send![class!(NSApplication), sharedApplication];
// Set activation policy to regular (shows in Dock)
// This ensures the window actually appears on screen
// 0 = NSApplicationActivationPolicyRegular
let _: () = msg_send![ns_app, setActivationPolicy: 0];
// Finish launching
let _: () = msg_send![ns_app, finishLaunching];
dprintln!("NSApplication initialized");
// Create the window
let window_rect = NSRect::new(NSPoint::new(x, y), NSSize::new(width, height));
dprintln!("Creating window at ({:.0}, {:.0}) size ({:.0}, {:.0})", x, y, width, height);
// Create window with borderless style mask
let window: id = msg_send![class!(NSWindow), alloc];
let window: id = msg_send![window,
initWithContentRect: window_rect
styleMask: 0 // NSBorderlessWindowMask
backing: 2 // NSBackingStoreBuffered
defer: 0];
// Set floating window level (above normal windows)
let level: i32 = NSFLOATING_WINDOW_LEVEL;
let _: () = msg_send![window, setLevel: level];
// CRITICAL: Set collection behavior for floating window on all spaces
// NSWindowCollectionBehaviorCanJoinAllSpaces = 1 << 3 = 8
// NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 16 = 65536
let collection_behavior: u64 = 8 | 65536;
let _: () = msg_send![window, setCollectionBehavior: collection_behavior];
// Set basic properties
let _: () = msg_send![window, setOpaque: 0];
// Note: setBackgroundColor with colorWithRed: causes crashes, skip for now
// Background is handled by NSBox subview instead
let _: () = msg_send![window, setAlphaValue: opacity as f32];
let _: () = msg_send![window, setHasShadow: 1];
// Get content view
let content_view: id = msg_send![window, contentView];
// Set the content view to have a solid dark background
let _: () = msg_send![content_view, setWantsLayer: YES];
let layer: id = msg_send![content_view, layer];
// Create a solid color layer for background
// Use simple dark gray
let bg_color: id = msg_send![class!(NSColor), darkGrayColor];
let _: () = msg_send![layer, setBackgroundColor: msg_send![bg_color, CGColor]];
// Set corner radius
let _: () = msg_send![layer, setCornerRadius: 12.0];
let _: () = msg_send![layer, setMasksToBounds: YES];
// Create text field for ASCII art display
let text_rect = NSRect::new(
NSPoint::new(5.0, 5.0),
NSSize::new(width - 10.0, height - 10.0),
);
let text_field: id = msg_send![class!(NSTextField), alloc];
let text_field: id = msg_send![text_field, initWithFrame: text_rect];
// Configure text field
let _: () = msg_send![text_field, setBezeled: 0];
let _: () = msg_send![text_field, setSelectable: 0];
let _: () = msg_send![text_field, setAlignment: 1]; // NSCenterTextAlignment
let _: () = msg_send![text_field, setDrawsBackground: 0];
let _: () = msg_send![text_field, setEditable: 0];
// Set white text color
let white_color: id = msg_send![class!(NSColor), whiteColor];
let _: () = msg_send![text_field, setTextColor: white_color];
// Set font
let font: id = msg_send![class!(NSFont), systemFontOfSize: 12.0];
let _: () = msg_send![text_field, setFont: font];
// Set string value with default content
let default_text = NSString::alloc(nil).init_str("Zen Widget\nLoading...");
let _: () = msg_send![text_field, setStringValue: default_text];
// Add text field to content view (on top of background)
let _: () = msg_send![content_view, addSubview: text_field];
// Show window
let _: () = msg_send![window, makeKeyAndOrderFront: nil];
// Force the window to become visible
let _: () = msg_send![window, orderFrontRegardless];
// Activate the app to ensure window appears
let _: () = msg_send![ns_app, activateIgnoringOtherApps: YES];
dprintln!("Floating window created successfully at ({:.0}, {:.0})", x, y);
Self {
window,
text_field,
width,
height,
}
}
}
pub fn update_content(&self, content: &str) {
unsafe {
let ns_string = NSString::alloc(nil).init_str(content);
let _: () = msg_send![self.text_field, setStringValue: ns_string];
}
dprintln!("Content updated: {} chars", content.len());
}
pub fn set_position(&self, x: f64, y: f64) {
unsafe {
let new_origin = NSPoint::new(x, y);
let _: () = msg_send![self.window, setFrameOrigin: new_origin];
}
dprintln!("Position set to ({:.0}, {:.0})", x, y);
}
pub fn show(&self) {
unsafe {
let _: () = msg_send![self.window, orderFront: nil];
}
dprintln!("Window shown");
}
pub fn hide(&self) {
unsafe {
let _: () = msg_send![self.window, orderOut: nil];
}
dprintln!("Window hidden");
}
/// Run the Cocoa event loop
/// Returns when stop() is called or app terminates
pub fn run(&self) {
unsafe {
let ns_app: id = msg_send![class!(NSApplication), sharedApplication];
// Run the event loop manually
loop {
// Get next event (with timeout)
let date: id = msg_send![class!(NSDate), dateWithTimeIntervalSinceNow: 0.001];
let event: id = msg_send![ns_app,
nextEventMatchingMask: u64::MAX
untilDate: date
inMode: NSDefaultRunLoopMode
dequeue: YES];
if event != nil {
let _: () = msg_send![ns_app, sendEvent: event];
}
// Update windows
let _: () = msg_send![ns_app, updateWindows];
// Check if we should stop (we could use a flag here)
// For now, just keep running
}
}
}
/// Stop the event loop (call this to exit)
pub fn stop(&self) {
unsafe {
let ns_app: id = msg_send![class!(NSApplication), sharedApplication];
let _: () = msg_send![ns_app, stop: nil];
// Post a dummy event to wake up the run loop
let event: id = msg_send![class!(NSEvent), otherEventWithType:0
location:NSPoint::new(0.0, 0.0)
modifierFlags:0
timestamp:0.0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
let _: () = msg_send![ns_app, postEvent: event atStart: YES];
}
}
}
/// Create a floating widget window
pub fn create_floating_window(
x: f64,
y: f64,
width: f64,
height: f64,
opacity: f64,
) -> Option<FloatingWindow> {
Some(FloatingWindow::new(x, y, width, height, opacity))
}
// ============================================================================
// Status Bar Menu
// ============================================================================
pub struct StatusBarMenu {
status_item: id,
menu: id,
}
impl StatusBarMenu {
pub fn new() -> Self {
unsafe {
// Get the system status bar using message send
let status_bar: id = msg_send![class!(NSStatusBar), systemStatusBar];
// Create status item with fixed length (gives more consistent appearance)
let status_item: id = msg_send![status_bar,
statusItemWithLength: 50.0]; // Fixed width
// Retain to prevent deallocation
let status_item: id = msg_send![status_item, retain];
// Create the menu
let menu: id = msg_send![class!(NSMenu), alloc];
let menu: id = msg_send![menu, init];
let menu: id = msg_send![menu, retain];
// Get the button from the status item
let button: id = msg_send![status_item, button];
// Set title for the button with a symbol
let title = NSString::alloc(nil).init_str("Z");
let _: () = msg_send![button, setTitle: title];
// Set a tooltip
let tooltip = NSString::alloc(nil).init_str("Zen Widget");
let _: () = msg_send![button, setToolTip: tooltip];
// Make button appear highlighted when menu is open
let _: () = msg_send![button, setHighlight: NO];
// Set the menu on the status item
let _: () = msg_send![status_item, setMenu: menu];
dprintln!("Status bar created successfully");
Self {
status_item,
menu,
}
}
}
pub fn setup_menu<F: Fn(String) + Send + 'static>(&mut self, _on_creature_change: F)
where F: Clone + 'static
{
unsafe {
// Clear existing menu
let _: () = msg_send![self.menu, removeAllItems];
// Header
let title = NSString::alloc(nil).init_str("Zen Widget");
let empty_key = NSString::alloc(nil).init_str("");
let header_item: id = msg_send![class!(NSMenuItem), alloc];
let header_item: id = msg_send![header_item,
initWithTitle:title
action:nil
keyEquivalent:empty_key];
let _: () = msg_send![header_item, setEnabled:NO];
self.menu.addItem_(header_item);
// Creature selection
let creatures = vec!["ghost", "cat", "octopus", "bean", "slime"];
for creature in creatures {
let title = NSString::alloc(nil).init_str(creature);
let empty_key = NSString::alloc(nil).init_str("");
let item: id = msg_send![class!(NSMenuItem), alloc];
let item: id = msg_send![item,
initWithTitle:title
action:sel!(selectCreature:)
keyEquivalent:empty_key];
self.menu.addItem_(item);
}
// Debug options
let title = NSString::alloc(nil).init_str("Toggle Debug Mode (d)");
let key = NSString::alloc(nil).init_str("d");
let debug_item: id = msg_send![class!(NSMenuItem), alloc];
let debug_item: id = msg_send![debug_item,
initWithTitle:title
action:sel!(toggleDebug:)
keyEquivalent:key];
self.menu.addItem_(debug_item);
let title = NSString::alloc(nil).init_str("Show Window Info (i)");
let key = NSString::alloc(nil).init_str("i");
let info_item: id = msg_send![class!(NSMenuItem), alloc];
let info_item: id = msg_send![info_item,
initWithTitle:title
action:sel!(showDebugInfo:)
keyEquivalent:key];
self.menu.addItem_(info_item);
// Quit
let title = NSString::alloc(nil).init_str("Quit (q)");
let key = NSString::alloc(nil).init_str("q");
let quit_item: id = msg_send![class!(NSMenuItem), alloc];
let quit_item: id = msg_send![quit_item,
initWithTitle:title
action:sel!(quitApp:)
keyEquivalent:key];
self.menu.addItem_(quit_item);
dprintln!("Menu items added");
}
}
}
/// Get screen dimensions and calculate widget position
pub fn get_widget_position(corner: &Corner, widget_width: u32, widget_height: u32) -> Option<(f64, f64)> {
let screen = get_main_display()?;
let offset = 20;
let (x, y) = corner.position(&screen, widget_width as f64, widget_height as f64, offset);
Some((x, y))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_corner_from_str() {
assert!(matches!(Corner::from_str("top-right"), Corner::TopRight));
assert!(matches!(Corner::from_str("topleft"), Corner::TopLeft));
assert!(matches!(Corner::from_str("invalid"), Corner::TopRight));
}
#[test]
fn test_widget_position() {
let screen = CGRect::new(CGDisplay::main().bounds().origin, CGDisplay::main().bounds().size);
let (x, y) = Corner::TopRight.position(&screen, 200.0, 150.0, 20);
assert!(x > 0.0);
assert!(y > 0.0);
}
}