Files
owlry/crates/owlry-plugin-emoji/src/lib.rs
vikingowl 384dd016a0 feat: convert to workspace with native plugin architecture
BREAKING: Restructure from monolithic binary to modular plugin ecosystem

Architecture changes:
- Convert to Cargo workspace with crates/ directory
- Create owlry-plugin-api crate with ABI-stable interface (abi_stable)
- Move core binary to crates/owlry/
- Extract providers to native plugin crates (13 plugins)
- Add owlry-lua crate for Lua plugin runtime

Plugin system:
- Plugins loaded from /usr/lib/owlry/plugins/*.so
- Widget providers refresh automatically (universal, not hardcoded)
- Per-plugin config via [plugins.<name>] sections in config.toml
- Backwards compatible with [providers] config format

New features:
- just install-local: build and install core + all plugins
- Plugin config: weather and pomodoro read from [plugins.*]
- HostAPI for plugins: notifications, logging

Documentation:
- Update README with new package structure
- Add docs/PLUGINS.md with all plugin documentation
- Add docs/PLUGIN_DEVELOPMENT.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 03:01:37 +01:00

562 lines
23 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Emoji Plugin for Owlry
//!
//! A static provider that provides emoji search and copy functionality.
//! Requires wl-clipboard (wl-copy) for copying to clipboard.
//!
//! Examples:
//! - Search "smile" → 😀 😃 😄 etc.
//! - Search "heart" → ❤️ 💙 💚 etc.
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
};
// Plugin metadata
const PLUGIN_ID: &str = "emoji";
const PLUGIN_NAME: &str = "Emoji";
const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION");
const PLUGIN_DESCRIPTION: &str = "Search and copy emojis";
// Provider metadata
const PROVIDER_ID: &str = "emoji";
const PROVIDER_NAME: &str = "Emoji";
const PROVIDER_PREFIX: &str = ":emoji";
const PROVIDER_ICON: &str = "face-smile";
const PROVIDER_TYPE_ID: &str = "emoji";
/// Emoji provider state - holds cached items
struct EmojiState {
items: Vec<PluginItem>,
}
impl EmojiState {
fn new() -> Self {
Self { items: Vec::new() }
}
fn load_emojis(&mut self) {
self.items.clear();
// Common emojis with searchable names
// Format: (emoji, name, keywords)
let emojis: &[(&str, &str, &str)] = &[
// Smileys & Emotion
("😀", "grinning face", "smile happy"),
("😃", "grinning face with big eyes", "smile happy"),
("😄", "grinning face with smiling eyes", "smile happy laugh"),
("😁", "beaming face with smiling eyes", "smile happy grin"),
("😅", "grinning face with sweat", "smile nervous"),
("🤣", "rolling on the floor laughing", "lol rofl funny"),
("😂", "face with tears of joy", "laugh cry funny lol"),
("🙂", "slightly smiling face", "smile"),
("😊", "smiling face with smiling eyes", "blush happy"),
("😇", "smiling face with halo", "angel innocent"),
("🥰", "smiling face with hearts", "love adore"),
("😍", "smiling face with heart-eyes", "love crush"),
("🤩", "star-struck", "excited wow amazing"),
("😘", "face blowing a kiss", "kiss love"),
("😜", "winking face with tongue", "playful silly"),
("🤪", "zany face", "crazy silly wild"),
("😎", "smiling face with sunglasses", "cool"),
("🤓", "nerd face", "geek glasses"),
("🧐", "face with monocle", "thinking inspect"),
("😏", "smirking face", "smug"),
("😒", "unamused face", "meh annoyed"),
("🙄", "face with rolling eyes", "whatever annoyed"),
("😬", "grimacing face", "awkward nervous"),
("😮‍💨", "face exhaling", "sigh relief"),
("🤥", "lying face", "pinocchio lie"),
("😌", "relieved face", "relaxed peaceful"),
("😔", "pensive face", "sad thoughtful"),
("😪", "sleepy face", "tired"),
("🤤", "drooling face", "hungry yummy"),
("😴", "sleeping face", "zzz tired"),
("😷", "face with medical mask", "sick covid"),
("🤒", "face with thermometer", "sick fever"),
("🤕", "face with head-bandage", "hurt injured"),
("🤢", "nauseated face", "sick gross"),
("🤮", "face vomiting", "sick puke"),
("🤧", "sneezing face", "achoo sick"),
("🥵", "hot face", "sweating heat"),
("🥶", "cold face", "freezing"),
("😵", "face with crossed-out eyes", "dizzy dead"),
("🤯", "exploding head", "mind blown wow"),
("🤠", "cowboy hat face", "yeehaw western"),
("🥳", "partying face", "celebration party"),
("🥸", "disguised face", "incognito"),
("🤡", "clown face", "circus"),
("👻", "ghost", "halloween spooky"),
("💀", "skull", "dead death"),
("☠️", "skull and crossbones", "danger death"),
("👽", "alien", "ufo extraterrestrial"),
("🤖", "robot", "bot android"),
("💩", "pile of poo", "poop"),
("😈", "smiling face with horns", "devil evil"),
("👿", "angry face with horns", "devil evil"),
// Gestures & People
("👋", "waving hand", "hello hi bye wave"),
("🤚", "raised back of hand", "stop"),
("🖐️", "hand with fingers splayed", "five high"),
("", "raised hand", "stop high five"),
("🖖", "vulcan salute", "spock trek"),
("👌", "ok hand", "okay perfect"),
("🤌", "pinched fingers", "italian"),
("🤏", "pinching hand", "small tiny"),
("✌️", "victory hand", "peace two"),
("🤞", "crossed fingers", "luck hope"),
("🤟", "love-you gesture", "ily rock"),
("🤘", "sign of the horns", "rock metal"),
("🤙", "call me hand", "shaka hang loose"),
("👈", "backhand index pointing left", "left point"),
("👉", "backhand index pointing right", "right point"),
("👆", "backhand index pointing up", "up point"),
("👇", "backhand index pointing down", "down point"),
("☝️", "index pointing up", "one point"),
("👍", "thumbs up", "like yes good approve"),
("👎", "thumbs down", "dislike no bad"),
("", "raised fist", "power solidarity"),
("👊", "oncoming fist", "punch bump"),
("🤛", "left-facing fist", "fist bump"),
("🤜", "right-facing fist", "fist bump"),
("👏", "clapping hands", "applause bravo"),
("🙌", "raising hands", "hooray celebrate"),
("👐", "open hands", "hug"),
("🤲", "palms up together", "prayer"),
("🤝", "handshake", "agreement deal"),
("🙏", "folded hands", "prayer please thanks"),
("✍️", "writing hand", "write"),
("💪", "flexed biceps", "strong muscle"),
("🦾", "mechanical arm", "robot prosthetic"),
("🦵", "leg", "kick"),
("🦶", "foot", "kick"),
("👂", "ear", "listen hear"),
("👃", "nose", "smell"),
("🧠", "brain", "smart think"),
("👀", "eyes", "look see watch"),
("👁️", "eye", "see look"),
("👅", "tongue", "taste lick"),
("👄", "mouth", "lips kiss"),
// Hearts & Love
("❤️", "red heart", "love"),
("🧡", "orange heart", "love"),
("💛", "yellow heart", "love friendship"),
("💚", "green heart", "love"),
("💙", "blue heart", "love"),
("💜", "purple heart", "love"),
("🖤", "black heart", "love dark"),
("🤍", "white heart", "love pure"),
("🤎", "brown heart", "love"),
("💔", "broken heart", "heartbreak sad"),
("❤️‍🔥", "heart on fire", "passion love"),
("❤️‍🩹", "mending heart", "healing recovery"),
("💕", "two hearts", "love"),
("💞", "revolving hearts", "love"),
("💓", "beating heart", "love"),
("💗", "growing heart", "love"),
("💖", "sparkling heart", "love"),
("💘", "heart with arrow", "love cupid"),
("💝", "heart with ribbon", "love gift"),
("💟", "heart decoration", "love"),
// Animals
("🐶", "dog face", "puppy"),
("🐱", "cat face", "kitty"),
("🐭", "mouse face", ""),
("🐹", "hamster", ""),
("🐰", "rabbit face", "bunny"),
("🦊", "fox", ""),
("🐻", "bear", ""),
("🐼", "panda", ""),
("🐨", "koala", ""),
("🐯", "tiger face", ""),
("🦁", "lion", ""),
("🐮", "cow face", ""),
("🐷", "pig face", ""),
("🐸", "frog", ""),
("🐵", "monkey face", ""),
("🦄", "unicorn", "magic"),
("🐝", "bee", "honeybee"),
("🦋", "butterfly", ""),
("🐌", "snail", "slow"),
("🐛", "bug", "caterpillar"),
("🦀", "crab", ""),
("🐙", "octopus", ""),
("🐠", "tropical fish", ""),
("🐟", "fish", ""),
("🐬", "dolphin", ""),
("🐳", "whale", ""),
("🦈", "shark", ""),
("🐊", "crocodile", "alligator"),
("🐢", "turtle", ""),
("🦎", "lizard", ""),
("🐍", "snake", ""),
("🦖", "t-rex", "dinosaur"),
("🦕", "sauropod", "dinosaur"),
("🐔", "chicken", ""),
("🐧", "penguin", ""),
("🦅", "eagle", "bird"),
("🦆", "duck", ""),
("🦉", "owl", ""),
// Food & Drink
("🍎", "red apple", "fruit"),
("🍐", "pear", "fruit"),
("🍊", "orange", "tangerine fruit"),
("🍋", "lemon", "fruit"),
("🍌", "banana", "fruit"),
("🍉", "watermelon", "fruit"),
("🍇", "grapes", "fruit"),
("🍓", "strawberry", "fruit"),
("🍒", "cherries", "fruit"),
("🍑", "peach", "fruit"),
("🥭", "mango", "fruit"),
("🍍", "pineapple", "fruit"),
("🥥", "coconut", "fruit"),
("🥝", "kiwi", "fruit"),
("🍅", "tomato", "vegetable"),
("🥑", "avocado", ""),
("🥦", "broccoli", "vegetable"),
("🥬", "leafy green", "vegetable salad"),
("🥒", "cucumber", "vegetable"),
("🌶️", "hot pepper", "spicy chili"),
("🌽", "corn", ""),
("🥕", "carrot", "vegetable"),
("🧄", "garlic", ""),
("🧅", "onion", ""),
("🥔", "potato", ""),
("🍞", "bread", ""),
("🥐", "croissant", ""),
("🥖", "baguette", "bread french"),
("🥨", "pretzel", ""),
("🧀", "cheese", ""),
("🥚", "egg", ""),
("🍳", "cooking", "frying pan egg"),
("🥞", "pancakes", "breakfast"),
("🧇", "waffle", "breakfast"),
("🥓", "bacon", "breakfast"),
("🍔", "hamburger", "burger"),
("🍟", "french fries", ""),
("🍕", "pizza", ""),
("🌭", "hot dog", ""),
("🥪", "sandwich", ""),
("🌮", "taco", "mexican"),
("🌯", "burrito", "mexican"),
("🍜", "steaming bowl", "ramen noodles"),
("🍝", "spaghetti", "pasta"),
("🍣", "sushi", "japanese"),
("🍱", "bento box", "japanese"),
("🍩", "doughnut", "donut dessert"),
("🍪", "cookie", "dessert"),
("🎂", "birthday cake", "dessert"),
("🍰", "shortcake", "dessert"),
("🧁", "cupcake", "dessert"),
("🍫", "chocolate bar", "dessert"),
("🍬", "candy", "sweet"),
("🍭", "lollipop", "candy sweet"),
("🍦", "soft ice cream", "dessert"),
("🍨", "ice cream", "dessert"),
("", "hot beverage", "coffee tea"),
("🍵", "teacup", "tea"),
("🧃", "juice box", ""),
("🥤", "cup with straw", "soda drink"),
("🍺", "beer mug", "drink alcohol"),
("🍻", "clinking beer mugs", "cheers drink"),
("🥂", "clinking glasses", "champagne cheers"),
("🍷", "wine glass", "drink alcohol"),
("🥃", "tumbler glass", "whiskey drink"),
("🍸", "cocktail glass", "martini drink"),
// Objects & Symbols
("💻", "laptop", "computer"),
("🖥️", "desktop computer", "pc"),
("⌨️", "keyboard", ""),
("🖱️", "computer mouse", ""),
("💾", "floppy disk", "save"),
("💿", "optical disk", "cd"),
("📱", "mobile phone", "smartphone"),
("☎️", "telephone", "phone"),
("📧", "email", "mail"),
("📨", "incoming envelope", "email"),
("📩", "envelope with arrow", "email send"),
("📝", "memo", "note write"),
("📄", "page facing up", "document"),
("📃", "page with curl", "document"),
("📑", "bookmark tabs", ""),
("📚", "books", "library read"),
("📖", "open book", "read"),
("🔗", "link", "chain url"),
("📎", "paperclip", "attachment"),
("🔒", "locked", "security"),
("🔓", "unlocked", "security open"),
("🔑", "key", "password"),
("🔧", "wrench", "tool fix"),
("🔨", "hammer", "tool"),
("⚙️", "gear", "settings"),
("🧲", "magnet", ""),
("💡", "light bulb", "idea"),
("🔦", "flashlight", ""),
("🔋", "battery", "power"),
("🔌", "electric plug", "power"),
("💰", "money bag", ""),
("💵", "dollar", "money cash"),
("💳", "credit card", "payment"),
("", "alarm clock", "time"),
("⏱️", "stopwatch", "timer"),
("📅", "calendar", "date"),
("📆", "tear-off calendar", "date"),
("", "check mark", "done yes"),
("", "cross mark", "no wrong delete"),
("", "question mark", "help"),
("", "exclamation mark", "important warning"),
("⚠️", "warning", "caution alert"),
("🚫", "prohibited", "no ban forbidden"),
("", "hollow circle", ""),
("🔴", "red circle", ""),
("🟠", "orange circle", ""),
("🟡", "yellow circle", ""),
("🟢", "green circle", ""),
("🔵", "blue circle", ""),
("🟣", "purple circle", ""),
("", "black circle", ""),
("", "white circle", ""),
("🟤", "brown circle", ""),
("", "black square", ""),
("", "white square", ""),
("🔶", "large orange diamond", ""),
("🔷", "large blue diamond", ""),
("", "star", "favorite"),
("🌟", "glowing star", "sparkle"),
("", "sparkles", "magic shine"),
("💫", "dizzy", "star"),
("🔥", "fire", "hot lit"),
("💧", "droplet", "water"),
("🌊", "wave", "water ocean"),
("🎵", "musical note", "music"),
("🎶", "musical notes", "music"),
("🎤", "microphone", "sing karaoke"),
("🎧", "headphones", "music"),
("🎮", "video game", "gaming controller"),
("🕹️", "joystick", "gaming"),
("🎯", "direct hit", "target bullseye"),
("🏆", "trophy", "winner award"),
("🥇", "1st place medal", "gold winner"),
("🥈", "2nd place medal", "silver"),
("🥉", "3rd place medal", "bronze"),
("🎁", "wrapped gift", "present"),
("🎈", "balloon", "party"),
("🎉", "party popper", "celebration tada"),
("🎊", "confetti ball", "celebration"),
// Arrows & Misc
("➡️", "right arrow", ""),
("⬅️", "left arrow", ""),
("⬆️", "up arrow", ""),
("⬇️", "down arrow", ""),
("↗️", "up-right arrow", ""),
("↘️", "down-right arrow", ""),
("↙️", "down-left arrow", ""),
("↖️", "up-left arrow", ""),
("↕️", "up-down arrow", ""),
("↔️", "left-right arrow", ""),
("🔄", "counterclockwise arrows", "refresh reload"),
("🔃", "clockwise arrows", "refresh reload"),
("", "plus", "add"),
("", "minus", "subtract"),
("", "division", "divide"),
("✖️", "multiply", "times"),
("♾️", "infinity", "forever"),
("💯", "hundred points", "100 perfect"),
("🆗", "ok button", "okay"),
("🆕", "new button", ""),
("🆓", "free button", ""),
("", "information", "info"),
("🅿️", "parking", ""),
("🚀", "rocket", "launch startup"),
("✈️", "airplane", "travel flight"),
("🚗", "car", "automobile"),
("🚕", "taxi", "cab"),
("🚌", "bus", ""),
("🚂", "locomotive", "train"),
("🏠", "house", "home"),
("🏢", "office building", "work"),
("🏥", "hospital", ""),
("🏫", "school", ""),
("🏛️", "classical building", ""),
("", "church", ""),
("🕌", "mosque", ""),
("🕍", "synagogue", ""),
("🗽", "statue of liberty", "usa america"),
("🗼", "tokyo tower", "japan"),
("🗾", "map of japan", ""),
("🌍", "globe europe-africa", "earth world"),
("🌎", "globe americas", "earth world"),
("🌏", "globe asia-australia", "earth world"),
("🌑", "new moon", ""),
("🌕", "full moon", ""),
("☀️", "sun", "sunny"),
("🌙", "crescent moon", "night"),
("☁️", "cloud", ""),
("🌧️", "cloud with rain", "rainy"),
("⛈️", "cloud with lightning", "storm thunder"),
("🌈", "rainbow", ""),
("❄️", "snowflake", "cold winter"),
("☃️", "snowman", "winter"),
("🎄", "christmas tree", "xmas holiday"),
("🎃", "jack-o-lantern", "halloween pumpkin"),
("🐚", "shell", "beach"),
("🌸", "cherry blossom", "flower spring"),
("🌺", "hibiscus", "flower"),
("🌻", "sunflower", "flower"),
("🌹", "rose", "flower love"),
("🌷", "tulip", "flower"),
("🌱", "seedling", "plant grow"),
("🌲", "evergreen tree", ""),
("🌳", "deciduous tree", ""),
("🌴", "palm tree", "tropical"),
("🌵", "cactus", "desert"),
("🍀", "four leaf clover", "luck irish"),
("🍁", "maple leaf", "fall autumn canada"),
("🍂", "fallen leaf", "fall autumn"),
];
for (emoji, name, keywords) in emojis {
self.items.push(
PluginItem::new(
format!("emoji:{}", emoji),
name.to_string(),
format!("printf '%s' '{}' | wl-copy", emoji),
)
.with_description(format!("{} {}", emoji, keywords))
.with_keywords(vec![name.to_string(), keywords.to_string()]),
);
}
}
}
// ============================================================================
// Plugin Interface Implementation
// ============================================================================
extern "C" fn plugin_info() -> PluginInfo {
PluginInfo {
id: RString::from(PLUGIN_ID),
name: RString::from(PLUGIN_NAME),
version: RString::from(PLUGIN_VERSION),
description: RString::from(PLUGIN_DESCRIPTION),
api_version: API_VERSION,
}
}
extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
vec![ProviderInfo {
id: RString::from(PROVIDER_ID),
name: RString::from(PROVIDER_NAME),
prefix: ROption::RSome(RString::from(PROVIDER_PREFIX)),
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Static,
type_id: RString::from(PROVIDER_TYPE_ID),
}]
.into()
}
extern "C" fn provider_init(_provider_id: RStr<'_>) -> ProviderHandle {
let state = Box::new(EmojiState::new());
ProviderHandle::from_box(state)
}
extern "C" fn provider_refresh(handle: ProviderHandle) -> RVec<PluginItem> {
if handle.ptr.is_null() {
return RVec::new();
}
// SAFETY: We created this handle from Box<EmojiState>
let state = unsafe { &mut *(handle.ptr as *mut EmojiState) };
// Load emojis
state.load_emojis();
// Return items
state.items.to_vec().into()
}
extern "C" fn provider_query(_handle: ProviderHandle, _query: RStr<'_>) -> RVec<PluginItem> {
// Static provider - query is handled by the core using cached items
RVec::new()
}
extern "C" fn provider_drop(handle: ProviderHandle) {
if !handle.ptr.is_null() {
// SAFETY: We created this handle from Box<EmojiState>
unsafe {
handle.drop_as::<EmojiState>();
}
}
}
// Register the plugin vtable
owlry_plugin! {
info: plugin_info,
providers: plugin_providers,
init: provider_init,
refresh: provider_refresh,
query: provider_query,
drop: provider_drop,
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emoji_state_new() {
let state = EmojiState::new();
assert!(state.items.is_empty());
}
#[test]
fn test_emoji_count() {
let mut state = EmojiState::new();
state.load_emojis();
assert!(state.items.len() > 100, "Should have more than 100 emojis");
}
#[test]
fn test_emoji_has_grinning_face() {
let mut state = EmojiState::new();
state.load_emojis();
let grinning = state
.items
.iter()
.find(|i| i.name.as_str() == "grinning face");
assert!(grinning.is_some());
let item = grinning.unwrap();
assert!(item.description.as_ref().unwrap().as_str().contains("😀"));
}
#[test]
fn test_emoji_command_format() {
let mut state = EmojiState::new();
state.load_emojis();
let item = &state.items[0];
assert!(item.command.as_str().contains("wl-copy"));
assert!(item.command.as_str().contains("printf"));
}
#[test]
fn test_emojis_have_keywords() {
let mut state = EmojiState::new();
state.load_emojis();
// Check that items have keywords for searching
let heart = state
.items
.iter()
.find(|i| i.name.as_str() == "red heart");
assert!(heart.is_some());
}
}