diff --git a/Cargo.lock b/Cargo.lock index 250064d..25f5eb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "cc" +version = "1.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-expr" version = "0.17.2" @@ -188,7 +197,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -289,12 +298,30 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "emoji" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e9309870371f7fa7767752e5048fc0c0675b017a27d9c601dffe9a1efe0301" +dependencies = [ + "fuzzy-matcher", + "itertools", + "lazy_static", + "phf", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -340,6 +367,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "field-offset" version = "0.3.6" @@ -412,7 +445,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -435,6 +468,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "gdk-pixbuf" version = "0.20.9" @@ -493,6 +535,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -501,7 +554,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -585,7 +650,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -721,7 +786,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -796,6 +861,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -823,7 +897,7 @@ checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -832,6 +906,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.171" @@ -854,6 +934,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -937,6 +1023,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -977,6 +1073,50 @@ dependencies = [ "indexmap 2.9.0", ] +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1010,6 +1150,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -1019,6 +1168,12 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -1038,6 +1193,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -1047,6 +1211,63 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1073,7 +1294,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -1116,6 +1337,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys", +] + [[package]] name = "rustix" version = "1.0.5" @@ -1125,7 +1359,7 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys", ] @@ -1158,7 +1392,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1182,6 +1416,18 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -1209,6 +1455,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.100" @@ -1239,6 +1496,19 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.5", + "windows-sys", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1271,7 +1541,17 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -1345,12 +1625,97 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wayland-backend" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +dependencies = [ + "bitflags 2.9.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "pkg-config", +] + [[package]] name = "which" version = "7.0.3" @@ -1359,7 +1724,7 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix", + "rustix 1.0.5", "winsafe", ] @@ -1482,6 +1847,34 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 0.38.44", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "worf" version = "0.1.0" @@ -1490,6 +1883,7 @@ dependencies = [ "clap 4.5.35", "crossbeam", "dirs", + "emoji", "env_logger", "freedesktop-file-parser", "gdk4", @@ -1508,6 +1902,7 @@ dependencies = [ "toml", "tree_magic_mini", "which", + "wl-clipboard-rs", ] [[package]] @@ -1517,7 +1912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeac9c0125f3c131c6a2898d2a9f25c11b7954c3ff644a018cb9e06fa92919b" dependencies = [ "clap 3.2.25", - "quick-xml", + "quick-xml 0.21.0", "serde", "tini", ] @@ -1536,3 +1931,23 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/Cargo.toml b/Cargo.toml index 70dec6d..ca23d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,5 @@ meval = "0.2.0" tree_magic_mini = "3.1.6" rayon = "1.10.0" nix = { version = "0.30.0", features = ["process"] } +emoji = "0.2.1" +wl-clipboard-rs = "0.9.2" diff --git a/README.md b/README.md index 99b6161..ea460cb 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,24 @@ It supports various modes: * File * Ssh * Run +* Emoji * // WebSearch -* // Emoji + * Auto -Auto mode tries to detect the desired mode automatically, i.e. `ssh`, `?` (for web search), `emoji`, `/` or `~` (for file). +Auto mode tries to detect the desired mode automatically, to achieve this some modes require a prefix in the search. +The standard view will show `ssh` and `drun`, for other modes the following prefixes are available: +* `ssh` (optional) +* `?` web search +* `/`, `$` or `~` for files +* `emoji` for emojis - + ## Not finished * [ ] key support * [ ] full config support * [ ] web search mode -* [ ] emoji finder * [ ] publish library diff --git a/src/lib/config.rs b/src/lib/config.rs index 9ab422b..f288c9c 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -1,27 +1,13 @@ use std::path::PathBuf; use std::str::FromStr; -use std::{env, fmt, fs}; +use std::{env, fs}; -use anyhow::anyhow; +use crate::Error; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; -#[derive(Debug)] -pub enum ConfigurationError { - Open(String), - Parse(String), -} - -impl fmt::Display for ConfigurationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ConfigurationError::Open(e) | ConfigurationError::Parse(e) => write!(f, "{e}"), - } - } -} - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] pub enum Anchor { Top, @@ -79,6 +65,9 @@ pub enum Mode { /// Connect via ssh to a given host Ssh, + + /// Emoji browser + Emoji, } #[derive(Debug, Error)] @@ -112,6 +101,7 @@ impl FromStr for Mode { "file" => Ok(Mode::File), "math" => Ok(Mode::Math), "ssh" => Ok(Mode::Ssh), + "emoji" => Ok(Mode::Emoji), "auto" => Ok(Mode::Auto), _ => Err(ArgsError::InvalidParameter( format!("{s} is not a valid argument, see help for details").to_owned(), @@ -188,8 +178,9 @@ pub struct Config { #[clap(short = 'I', long = "allow-images")] allow_images: Option, + /// If `true` pango markup is parsed #[clap(short = 'm', long = "allow-markup")] - allow_markup: Option, // todo support this + allow_markup: Option, #[clap(short = 'k', long = "cache-file")] cache_file: Option, // todo support this @@ -223,6 +214,8 @@ pub struct Config { #[clap(short = 'M', long = "matching")] matching: Option, + /// Control if search is case-insensitive or not. + /// Defaults to true #[clap(short = 'i', long = "insensitive")] insensitive: Option, @@ -249,7 +242,7 @@ pub struct Config { sort_order: Option, #[clap(short = 'Q', long = "search")] - search: Option, // todo support this + search: Option, #[clap(short = 'o', long = "monitor")] monitor: Option, // todo support this @@ -405,6 +398,7 @@ impl Config { Mode::File => "file".to_owned(), Mode::Auto => "auto".to_owned(), Mode::Ssh => "ssh".to_owned(), + Mode::Emoji => "emoji".to_owned(), }, }, @@ -484,12 +478,26 @@ impl Config { pub fn hide_search(&self) -> bool { self.hide_search.unwrap_or(false) } + + #[must_use] + pub fn search(&self) -> Option { + self.search.clone() + } + + #[must_use] + pub fn allow_markup(&self) -> bool { + self.allow_markup.unwrap_or(false) + } } fn default_false() -> bool { false } +// fn default_true() -> bool { +// true +// } + // // // TODO // // GtkOrientation orientation = config_get_mnemonic(config, "orientation", "vertical", 2, "vertical", "horizontal"); @@ -503,7 +511,6 @@ fn default_false() -> bool { // // GtkAlign valign = config_get_mnemonic(config, "valign", default_valign, 4, "fill", "start", "end", "center"); // // char* prompt = config_get(config, "prompt", mode); // // uint64_t filter_rate = strtol(config_get(config, "filter_rate", "100"), NULL, 10); -// // allow_images = strcmp(config_get(config, "allow_images", "false"), "true") == 0; // // allow_markup = strcmp(config_get(config, "allow_markup", "false"), "true") == 0; // // image_size = strtol(config_get(config, "image_size", "32"), NULL, 10); // // cache_file = map_get(config, "cache_file"); @@ -576,7 +583,7 @@ pub fn parse_args() -> Config { /// # Errors /// /// Will return Err when it cannot resolve any path or no style is found -fn style_path(full_path: Option) -> Result { +fn style_path(full_path: Option) -> Result { let alternative_paths = path_alternatives( vec![dirs::config_dir()], &PathBuf::from("worf").join("style.css"), @@ -587,7 +594,7 @@ fn style_path(full_path: Option) -> Result { /// # Errors /// /// Will return Err when it cannot resolve any path or no style is found -pub fn conf_path(full_path: Option) -> Result { +pub fn conf_path(full_path: Option) -> Result { let alternative_paths = path_alternatives( vec![dirs::config_dir()], &PathBuf::from("worf").join("config"), @@ -612,7 +619,7 @@ pub fn path_alternatives(base_paths: Vec>, sub_path: &PathBuf) - pub fn resolve_path( full_path: Option, alternatives: Vec, -) -> Result { +) -> Result { full_path .map(PathBuf::from) .and_then(|p| p.canonicalize().ok().filter(|c| c.exists())) @@ -622,7 +629,7 @@ pub fn resolve_path( .filter(|p| p.exists()) .find_map(|pb| pb.canonicalize().ok().filter(|c| c.exists())) }) - .ok_or_else(|| anyhow!("Could not find a valid file.")) + .ok_or(Error::MissingFile) } /// # Errors @@ -632,25 +639,24 @@ pub fn resolve_path( /// * cannot parse the config file /// * no config file exists /// * config file and args cannot be merged -pub fn load_config(args_opt: Option) -> Result { +pub fn load_config(args_opt: Option<&Config>) -> Result { let config_path = conf_path(args_opt.as_ref().and_then(|c| c.config.clone())); match config_path { Ok(path) => { - let toml_content = - fs::read_to_string(path).map_err(|e| ConfigurationError::Open(format!("{e}")))?; - let mut config: Config = toml::from_str(&toml_content) - .map_err(|e| ConfigurationError::Parse(format!("{e}")))?; + let toml_content = fs::read_to_string(path).map_err(|e| Error::Io(format!("{e}")))?; + let mut config: Config = + toml::from_str(&toml_content).map_err(|e| Error::ParsingError(format!("{e}")))?; if let Some(args) = args_opt { - let merge_result = merge_config_with_args(&mut config, &args) - .map_err(|e| ConfigurationError::Parse(format!("{e}")))?; + let merge_result = merge_config_with_args(&mut config, args) + .map_err(|e| Error::ParsingError(format!("{e}")))?; Ok(merge_result) } else { Ok(config) } } - Err(e) => Err(ConfigurationError::Open(format!("{e}"))), + Err(e) => Err(Error::Io(format!("{e}"))), } } diff --git a/src/lib/desktop.rs b/src/lib/desktop.rs index 9a2d538..eebc34e 100644 --- a/src/lib/desktop.rs +++ b/src/lib/desktop.rs @@ -1,8 +1,3 @@ -use crate::Error; -use crate::config::expand_path; -use freedesktop_file_parser::DesktopFile; -use rayon::prelude::*; -use regex::Regex; use std::collections::HashMap; use std::os::unix::fs::PermissionsExt; use std::os::unix::prelude::CommandExt; @@ -12,6 +7,14 @@ use std::process::{Command, Stdio}; use std::time::Instant; use std::{env, fs, io}; +use freedesktop_file_parser::DesktopFile; +use rayon::prelude::*; +use regex::Regex; +use wl_clipboard_rs::copy::{ClipboardType, MimeType, ServeRequests, Source}; + +use crate::Error; +use crate::config::expand_path; + /// Returns a regex with supported image extensions /// # Panics /// @@ -291,3 +294,18 @@ pub fn is_executable(entry: &Path) -> bool { false } } + +/// Copy the given text into the clipboard. +/// # Errors +/// Will return an error if copying to the clipboard failed. +pub fn copy_to_clipboard(text: String) -> Result<(), Error> { + let mut opts = wl_clipboard_rs::copy::Options::new(); + opts.clipboard(ClipboardType::Regular); + opts.serve_requests(ServeRequests::Only(1)); + let result = opts.copy(Source::Bytes(text.into_bytes().into()), MimeType::Text); + + match result { + Ok(()) => Ok(()), + Err(e) => Err(Error::Clipboard(e.to_string())), + } +} diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 0814a1d..3bb393e 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -4,7 +4,6 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; -use anyhow::anyhow; use crossbeam::channel; use crossbeam::channel::Sender; use gdk4::gio::File; @@ -28,11 +27,11 @@ use regex::Regex; use crate::config::{Anchor, Config, MatchMethod, WrapMode}; use crate::desktop::known_image_extension_regex_pattern; -use crate::{config, desktop}; +use crate::{Error, config, desktop}; type ArcMenuMap = Arc>>>; type ArcProvider = Arc + Send>>; -type MenuItemSender = Sender, anyhow::Error>>; +type MenuItemSender = Sender, Error>>; pub trait ItemProvider { fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>); @@ -141,6 +140,7 @@ struct MetaData { selected_sender: MenuItemSender, config: Rc, new_on_empty: bool, + search_ignored_words: Option>, } struct UiElements { @@ -159,12 +159,13 @@ pub fn show( config: Config, item_provider: P, new_on_empty: bool, -) -> Result, anyhow::Error> + search_ignored_words: Option>, +) -> Result, Error> where T: Clone + 'static + Send, P: ItemProvider + 'static + Clone + Send, { - gtk4::init()?; + gtk4::init().map_err(|e| Error::Graphics(e.to_string()))?; log::debug!("Starting GUI"); if let Some(ref css) = config.style() { let provider = CssProvider::new(); @@ -189,20 +190,22 @@ where sender.clone(), app.clone(), new_on_empty, + search_ignored_words.clone(), ); }); let gtk_args: [&str; 0] = []; app.run_with_args(>k_args); - receiver.recv()? + receiver.recv().map_err(|e| Error::Io(e.to_string()))? } fn build_ui( config: &Config, item_provider: P, - sender: Sender, anyhow::Error>>, + sender: Sender, Error>>, app: Application, new_on_empty: bool, + search_ignored_words: Option>, ) where T: Clone + 'static + Send, P: ItemProvider + 'static + Send, @@ -214,6 +217,7 @@ fn build_ui( selected_sender: sender, config: Rc::new(config.clone()), new_on_empty, + search_ignored_words, }); let provider_clone = Arc::clone(&meta.item_provider); @@ -340,6 +344,9 @@ fn build_search_entry(config: &Config, ui_elements: &UiElements) { if config.hide_search() { ui_elements.search.set_visible(false); } + if let Some(search) = config.search() { + ui_elements.search.set_text(&search); + } } fn build_ui_from_menu_items( @@ -374,7 +381,12 @@ fn build_ui_from_menu_items( let query = ui_clone.search.text(); let menus = &mut *lock; - set_menu_visibility_for_search(&query, menus, &meta_clone.config); + set_menu_visibility_for_search( + &query, + menus, + &meta_clone.config, + meta_clone.search_ignored_words.as_ref(), + ); } let items_sort = ArcMenuMap::clone(&ui_clone.menu_rows); @@ -387,7 +399,11 @@ fn build_ui_from_menu_items( let menus = &mut *lock; select_first_visible_child(menus, &ui_clone.main_box); - log::debug!("Created menu items in {:?}", start.elapsed()); + log::debug!( + "Created {} menu items in {:?}", + menus.len(), + start.elapsed() + ); ControlFlow::Break } else { ControlFlow::Continue @@ -418,7 +434,12 @@ fn handle_key_press( let update_view = |query: &String| { let mut lock = ui.menu_rows.lock().unwrap(); let menus = &mut *lock; - set_menu_visibility_for_search(query, menus, &meta.config); + set_menu_visibility_for_search( + query, + menus, + &meta.config, + meta.search_ignored_words.as_ref(), + ); select_first_visible_child(&*lock, &ui.main_box); }; @@ -433,7 +454,7 @@ fn handle_key_press( match keyboard_key { Key::Escape => { - if let Err(e) = meta.selected_sender.send(Err(anyhow!("No item selected"))) { + if let Err(e) = meta.selected_sender.send(Err(Error::NoSelection)) { log::error!("failed to send message {e}"); } close_gui(ui.app.clone(), ui.window.clone(), &meta.config); @@ -790,15 +811,27 @@ fn create_menu_row( row_box.set_halign(Align::Fill); row.set_child(Some(&row_box)); + + let (label_img, label_text) = parse_label(&element_to_add.label); + if meta.config.allow_images() { - if let Some(image) = lookup_icon(element_to_add, &meta.config) { + let img = lookup_icon( + element_to_add.icon_path.as_ref().map(AsRef::as_ref), + &meta.config, + ) + .or(lookup_icon( + label_img.as_ref().map(AsRef::as_ref), + &meta.config, + )); + + if let Some(image) = img { image.set_widget_name("img"); row_box.append(&image); } } - let label = Label::new(Some(element_to_add.label.as_str())); - + let label = Label::new(label_text.as_ref().map(AsRef::as_ref)); + label.set_use_markup(meta.config.allow_markup()); label.set_natural_wrap_mode(meta.config.line_wrap().into()); label.set_hexpand(true); label.set_widget_name("text"); @@ -834,9 +867,49 @@ fn create_menu_row( row.upcast() } +fn parse_label(label: &str) -> (Option, Option) { + let mut img = None; + let mut text = None; + + let parts: Vec<&str> = label.split(':').collect(); + let mut i = 0; + + while i < parts.len() { + match parts.get(i) { + Some(&"img") => { + if i + 1 < parts.len() { + img = Some(parts[i + 1].to_string()); + i += 2; + } else { + i += 1; + } + } + Some(&"text") => { + i += 1; + let mut text_parts = Vec::new(); + while i < parts.len() && parts[i] != "img" && parts[i] != "text" { + text_parts.push(parts[i]); + i += 1; + } + text = Some(text_parts.join(":").trim().to_string()); + } + other => { + // Treat as fallback text if no text tag is present + if text.is_none() { + text = Some((*other.unwrap_or(&"")).to_string()); + } else { + text = Some(text.unwrap() + ":" + (*other.unwrap_or(&""))); + } + i += 1; + } + } + } + + (img, text) +} -fn lookup_icon(menu_item: &MenuItem, config: &Config) -> Option { - if let Some(image_path) = &menu_item.icon_path { +fn lookup_icon(icon_path: Option<&str>, config: &Config) -> Option { + if let Some(image_path) = icon_path { let img_regex = Regex::new(&format!( r"((?i).*{})", known_image_extension_regex_pattern() @@ -864,6 +937,7 @@ fn set_menu_visibility_for_search( query: &str, items: &mut HashMap>, config: &Config, + search_ignored_words: Option<&Vec>, ) { { if query.is_empty() { @@ -875,20 +949,37 @@ fn set_menu_visibility_for_search( return; } - let query = if config.insensitive() { + let mut query = if config.insensitive() { query.to_owned().to_lowercase() } else { query.to_owned() }; + + if let Some(s) = search_ignored_words.as_ref() { + s.iter().for_each(|rgx| { + query = rgx.replace_all(&query, "").to_string(); + }); + } + for (fb, menu_item) in items.iter_mut() { let menu_item_search = format!( "{} {}", menu_item .action .as_ref() - .map(|a| a.to_lowercase()) + .map(|a| { + if config.insensitive() { + a.to_lowercase() + } else { + a.clone() + } + }) .unwrap_or_default(), - &menu_item.label.to_lowercase() + if config.insensitive() { + menu_item.label.to_lowercase() + } else { + menu_item.label.clone() + } ); let (search_sort_score, visible) = match config.match_method() { diff --git a/src/lib/mod.rs b/src/lib/mod.rs index d18dd69..f3fd6f2 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -22,6 +22,12 @@ pub enum Error { RunFailed(String), /// An IO operation failed Io(String), + /// An error occurred while accessing the clipboard + Clipboard(String), + /// Graphical subsystem related error + Graphics(String), + /// Nothing selected + NoSelection, } impl fmt::Display for Error { @@ -46,6 +52,15 @@ impl fmt::Display for Error { Error::Io(s) => { write!(f, "IO {s}") } + Error::Clipboard(s) => { + write!(f, "Clipboard {s}") + } + Error::Graphics(s) => { + write!(f, "graphics {s}") + } + Error::NoSelection => { + write!(f, "NoSelection") + } } } } diff --git a/src/lib/mode.rs b/src/lib/mode.rs index 94d9535..c3591db 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -1,7 +1,7 @@ use crate::config::{Config, expand_path}; use crate::desktop::{ - create_file_if_not_exists, find_desktop_files, get_locale_variants, is_executable, - load_cache_file, lookup_name_with_locale, save_cache_file, spawn_fork, + copy_to_clipboard, create_file_if_not_exists, find_desktop_files, get_locale_variants, + is_executable, load_cache_file, lookup_name_with_locale, save_cache_file, spawn_fork, }; use crate::gui::{ItemProvider, MenuItem}; use crate::{Error, gui}; @@ -309,7 +309,10 @@ impl ItemProvider for FileItemProvider { }; let mut trimmed_search = search.unwrap_or(&default_path).to_owned(); - if !trimmed_search.starts_with('/') && !trimmed_search.starts_with('~') { + if !trimmed_search.starts_with('/') + && !trimmed_search.starts_with('~') + && !trimmed_search.starts_with('$') + { trimmed_search = format!("{default_path}/{trimmed_search}"); } @@ -503,6 +506,49 @@ impl ItemProvider for MathProvider { } } +#[derive(Clone)] +struct EmojiProvider { + elements: Vec>, + #[allow(dead_code)] // needed for the detection of mode in 'auto' + menu_item_data: T, +} + +impl EmojiProvider { + fn new(data: T) -> Self { + let emoji = emoji::search::search_annotation_all(""); + let mut menus = emoji + .into_iter() + .map(|e| { + MenuItem::new( + format!("{} — Category: {} — Name: {}", e.glyph, e.group, e.name), + None, + Some(format!("emoji {}", e.glyph)), + vec![], + None, + 0.0, + Some(data.clone()), + ) + }) + .collect::>(); + gui::sort_menu_items_alphabetically_honor_initial_score(&mut menus); + + Self { + elements: menus, + menu_item_data: data.clone(), + } + } +} + +impl ItemProvider for EmojiProvider { + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { + (false, self.elements.clone()) + } + + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { + (false, None) + } +} + #[derive(Clone)] struct DMenuProvider { items: Vec>, @@ -541,9 +587,8 @@ enum AutoRunType { DRun, File, Ssh, + Emoji, // WebSearch, - // Emoji, - // Run, } #[derive(Clone)] @@ -552,6 +597,7 @@ struct AutoItemProvider { file: FileItemProvider, math: MathProvider, ssh: SshProvider, + emoji: EmojiProvider, last_mode: Option, } @@ -562,6 +608,7 @@ impl AutoItemProvider { file: FileItemProvider::new(AutoRunType::File), math: MathProvider::new(AutoRunType::Math), ssh: SshProvider::new(AutoRunType::Ssh), + emoji: EmojiProvider::new(AutoRunType::Emoji), last_mode: None, } } @@ -597,6 +644,8 @@ impl ItemProvider for AutoItemProvider { (AutoRunType::File, self.file.get_elements(search_opt)) } else if search.starts_with("ssh") { (AutoRunType::Ssh, self.ssh.get_elements(search_opt)) + } else if search.starts_with("emoji") { + (AutoRunType::Emoji, self.emoji.get_elements(search_opt)) } else { return self.default_auto_elements(search_opt); }; @@ -623,12 +672,12 @@ impl ItemProvider for AutoItemProvider { /// /// Will return `Err` if it was not able to spawn the process pub fn d_run(config: &Config) -> Result<(), Error> { - let provider = DRunProvider::new(String::new()); + let provider = DRunProvider::new(0); let cache_path = provider.cache_path.clone(); let mut cache = provider.cache.clone(); // todo ues a arc instead of cloning the config - let selection_result = gui::show(config.clone(), provider, false); + let selection_result = gui::show(config.clone(), provider, false, None); match selection_result { Ok(s) => update_drun_cache_and_run(cache_path, &mut cache, s)?, Err(_) => { @@ -648,7 +697,7 @@ pub fn run(config: &Config) -> Result<(), Error> { let cache_path = provider.cache_path.clone(); let mut cache = provider.cache.clone(); - let selection_result = gui::show(config.clone(), provider, false); + let selection_result = gui::show(config.clone(), provider, false, None); match selection_result { Ok(s) => update_run_cache_and_run(cache_path, &mut cache, s)?, Err(_) => { @@ -664,6 +713,9 @@ pub fn run(config: &Config) -> Result<(), Error> { /// /// Will return `Err` /// * if it was not able to spawn the process +/// +/// # Panics +/// Panics if an internal static regex cannot be passed anymore, should never happen pub fn auto(config: &Config) -> Result<(), Error> { let mut provider = AutoItemProvider::new(); let cache_path = provider.drun.cache_path.clone(); @@ -671,7 +723,17 @@ pub fn auto(config: &Config) -> Result<(), Error> { loop { // todo ues a arc instead of cloning the config - let selection_result = gui::show(config.clone(), provider.clone(), true); + let selection_result = gui::show( + config.clone(), + provider.clone(), + true, + Some( + vec!["ssh", "emoji", "^\\$\\w+"] + .into_iter() + .map(|s| Regex::new(s).unwrap()) + .collect(), + ), + ); if let Ok(mut selection_result) = selection_result { if let Some(data) = &selection_result.data { @@ -693,6 +755,14 @@ pub fn auto(config: &Config) -> Result<(), Error> { ssh_launch(&selection_result, config)?; break; } + AutoRunType::Emoji => { + if let Some(action) = selection_result.action { + copy_to_clipboard(action)?; + } else { + return Err(Error::MissingAction); + } + break; + } } } else if selection_result.label.starts_with("ssh") { selection_result.label = selection_result.label.chars().skip(4).collect(); @@ -712,23 +782,24 @@ pub fn auto(config: &Config) -> Result<(), Error> { /// /// Will return `Err` /// * if it was not able to spawn the process +/// +/// # Panics +/// In case an internal regex does not parse anymore, this should never happen pub fn file(config: &Config) -> Result<(), Error> { - let provider = FileItemProvider::new(String::new()); + let provider = FileItemProvider::new(0); // todo ues a arc instead of cloning the config - let selection_result = gui::show(config.clone(), provider, false); - match selection_result { - Ok(s) => { - if let Some(action) = s.action { - spawn_fork(&action, s.working_dir.as_ref())?; - } - } - Err(_) => { - log::error!("No item selected"); - } + let selection_result = gui::show( + config.clone(), + provider, + false, + Some(vec![Regex::new("^\\$\\w+").unwrap()]), + )?; + if let Some(action) = selection_result.action { + spawn_fork(&action, selection_result.working_dir.as_ref()) + } else { + Err(Error::MissingAction) } - - Ok(()) } fn ssh_launch(menu_item: &MenuItem, config: &Config) -> Result<(), Error> { @@ -759,8 +830,8 @@ fn ssh_launch(menu_item: &MenuItem, config: &Config) -> Result<(), /// * if it was not able to spawn the process /// * if it didn't find a terminal pub fn ssh(config: &Config) -> Result<(), Error> { - let provider = SshProvider::new(String::new()); - let selection_result = gui::show(config.clone(), provider, true); + let provider = SshProvider::new(0); + let selection_result = gui::show(config.clone(), provider, true, None); if let Ok(mi) = selection_result { ssh_launch(&mi, config)?; } else { @@ -775,7 +846,7 @@ pub fn math(config: &Config) { loop { let mut provider = MathProvider::new(String::new()); provider.add_elements(&mut calc.clone()); - let selection_result = gui::show(config.clone(), provider, true); + let selection_result = gui::show(config.clone(), provider, true, None); if let Ok(mi) = selection_result { calc.push(mi); } else { @@ -785,6 +856,19 @@ pub fn math(config: &Config) { } } +/// Shows the emoji mode +/// # Errors +/// +/// Forwards errors from the gui. See `gui::show` for details. +pub fn emoji(config: &Config) -> Result<(), Error> { + let provider = EmojiProvider::new(0); + let selection_result = gui::show(config.clone(), provider, true, None)?; + match selection_result.action { + None => Err(Error::MissingAction), + Some(action) => copy_to_clipboard(action), + } +} + /// Shows the dmenu mode /// # Errors /// @@ -792,7 +876,7 @@ pub fn math(config: &Config) { pub fn dmenu(config: &Config) -> Result<(), Error> { let provider = DMenuProvider::new()?; - let selection_result = gui::show(config.clone(), provider, true); + let selection_result = gui::show(config.clone(), provider, true, None); match selection_result { Ok(s) => { println!("{}", s.label); diff --git a/src/main.rs b/src/main.rs index 3879f7f..58c16f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ fn main() -> anyhow::Result<()> { .init(); let args = config::parse_args(); - let config = config::load_config(Some(args)).map_err(|e| anyhow!(e))?; + let config = config::load_config(Some(&args)).unwrap_or(args); if let Some(show) = &config.show() { match show { @@ -32,6 +32,9 @@ fn main() -> anyhow::Result<()> { Mode::Ssh => { mode::ssh(&config).map_err(|e| anyhow!(e))?; } + Mode::Emoji => { + mode::emoji(&config).map_err(|e| anyhow!(e))?; + } Mode::Auto => { mode::auto(&config).map_err(|e| anyhow!(e))?; }