initial
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+543
@@ -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
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user