diff --git a/MANIFEST.in b/MANIFEST.in index 962aa000..6f4000db 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ recursive-include doc/source * +recursive-include tools include data/poezio.1 include data/io.poez.Poezio.appdata.xml include data/io.poez.Poezio.desktop diff --git a/doc/source/plugins/index.rst b/doc/source/plugins/index.rst index 42578be8..c1222c84 100644 --- a/doc/source/plugins/index.rst +++ b/doc/source/plugins/index.rst @@ -211,6 +211,11 @@ Plugin index Adds convenient aliases to /status (/away, etc). + Sticker + :ref:`Documentation ` + + Opens a graphical sticker picker and sends the selected one. + Tell :ref:`Documentation ` @@ -342,6 +347,7 @@ Plugin index simple_notify spam status + sticker tell time_marker uptime diff --git a/doc/source/plugins/sticker.rst b/doc/source/plugins/sticker.rst new file mode 100644 index 00000000..815fb141 --- /dev/null +++ b/doc/source/plugins/sticker.rst @@ -0,0 +1,6 @@ +.. _sticker-plugin: + +Sticker +======= + +.. automodule:: sticker diff --git a/plugins/sticker.py b/plugins/sticker.py new file mode 100644 index 00000000..c9deacc0 --- /dev/null +++ b/plugins/sticker.py @@ -0,0 +1,97 @@ +''' +This plugin lets the user select and send a sticker from a pack of stickers. + +The protocol used here is based on XEP-0363 and XEP-0066, while a future +version may use XEP-0449 instead. + +Command +------- + +.. glossary:: + /sticker + **Usage:** ``/sticker `` + + Opens a picker tool, and send the sticker which has been selected. + +Configuration options +--------------------- + +.. glossary:: + sticker_picker + **Default:** ``poezio-sticker-picker`` + + The command to invoke as a sticker picker. A sample one is provided in + tools/sticker-picker. + + stickers_dir + **Default:** ``XDG_DATA_HOME/poezio/stickers`` + + The directory under which the sticker packs can be found. +''' + +import asyncio +import concurrent.futures +from poezio import xdg +from poezio.plugin import BasePlugin +from poezio.config import config +from poezio.decorators import command_args_parser +from poezio.core.structs import Completion +from pathlib import Path +from asyncio.subprocess import PIPE, DEVNULL + +class Plugin(BasePlugin): + dependencies = {'upload'} + + def init(self): + # The command to use as a picker helper. + self.picker_command = config.getstr('sticker_picker') or 'poezio-sticker-picker' + + # Select and create the stickers directory. + directory = config.getstr('stickers_dir') + if directory: + self.directory = Path(directory).expanduser() + else: + self.directory = xdg.DATA_HOME / 'stickers' + self.directory.mkdir(parents=True, exist_ok=True) + + self.upload = self.refs['upload'] + self.api.add_command('sticker', self.command_sticker, + usage='', + short='Send a sticker', + help='Send a sticker, with a helper GUI sticker picker', + completion=self.completion_sticker) + + def command_sticker(self, pack): + ''' + Sends a sticker + ''' + if not pack: + self.api.information('Missing sticker pack argument.', 'Error') + return + async def run_command(tab, path: Path): + try: + process = await asyncio.create_subprocess_exec( + self.picker_command, path, stdout=PIPE, stderr=PIPE) + sticker, stderr = await process.communicate() + except FileNotFoundError as err: + self.api.information('Failed to launch the sticker picker: %s' % err, 'Error') + return + else: + if process.returncode != 0: + self.api.information('Sticker picker failed: %s' % stderr.decode(), 'Error') + return + if sticker: + filename = sticker.decode().rstrip() + self.api.information('Sending sticker %s' % filename, 'Info') + await self.upload.send_upload(path / filename, tab) + tab = self.api.current_tab() + path = self.directory / pack + asyncio.create_task(run_command(tab, path)) + + def completion_sticker(self, the_input): + ''' + Completion for /sticker + ''' + txt = the_input.get_text()[9:] + directories = [directory.name for directory in self.directory.glob(txt + '*')] + return Completion(the_input.auto_completion, directories, quotify=False) diff --git a/tools/sticker-picker/Cargo.toml b/tools/sticker-picker/Cargo.toml new file mode 100644 index 00000000..fdba8144 --- /dev/null +++ b/tools/sticker-picker/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "poezio-sticker-picker" +version = "0.1.0" +edition = "2021" +authors = ["Emmanuel Gil Peyrot "] +license = "Zlib" +description = "Helper tool for selecting a sticker inside a pack" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gtk = { package = "gtk4", version = "0.4", features = ["v4_6"] } +gdk = { package = "gdk4", version = "0.4", features = ["v4_6"] } +glib = "0.15" +gio = "0.15" +once_cell = "1.9.0" diff --git a/tools/sticker-picker/src/main.rs b/tools/sticker-picker/src/main.rs new file mode 100644 index 00000000..49795f4d --- /dev/null +++ b/tools/sticker-picker/src/main.rs @@ -0,0 +1,93 @@ +// This file is part of Poezio. +// +// Poezio is free software: you can redistribute it and/or modify +// it under the terms of the zlib license. See the COPYING file. + +mod sticker; + +use gtk::prelude::*; +use sticker::StickerType as Sticker; + +fn main() { + let app = gtk::Application::builder() + .application_id("io.poez.StickerPicker") + .flags(gio::ApplicationFlags::HANDLES_OPEN) + .build(); + + let quit = gio::SimpleAction::new("quit", None); + app.set_accels_for_action("app.quit", &["q"]); + app.add_action(&quit); + quit.connect_activate(glib::clone!(@weak app => move |_, _| app.quit())); + + app.connect_open(move |app, directories, _| { + let path = match directories { + [directory] => directory.path().unwrap(), + _ => { + eprintln!("Only a single directory is allowed!"); + std::process::exit(1); + } + }; + + let window = gtk::ApplicationWindow::builder() + .application(app) + .default_width(1280) + .default_height(720) + .title("Poezio Sticker Picker") + .build(); + + let sw = gtk::ScrolledWindow::builder() + .has_frame(true) + .hscrollbar_policy(gtk::PolicyType::Always) + .vscrollbar_policy(gtk::PolicyType::Always) + .vexpand(true) + .build(); + window.set_child(Some(&sw)); + + let store = gio::ListStore::new(Sticker::static_type()); + + for dir_entry in std::fs::read_dir(path).unwrap() { + let dir_entry = dir_entry.unwrap(); + let file_name = dir_entry.file_name().into_string().unwrap(); + let sticker = Sticker::new(file_name, &dir_entry.path()); + store.append(&sticker); + } + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, item| { + let picture = gtk::Picture::builder() + .alternative_text("Sticker") + .can_shrink(false) + .build(); + item.set_child(Some(&picture)); + }); + factory.connect_bind(|_, list_item| { + if let Some(child) = list_item.child() { + if let Some(item) = list_item.item() { + let picture: gtk::Picture = child.downcast().unwrap(); + let sticker: Sticker = item.downcast().unwrap(); + picture.set_paintable(sticker.texture().as_ref()); + } + } + }); + + let selection = gtk::SingleSelection::new(Some(&store)); + let grid_view = gtk::GridView::builder() + .single_click_activate(true) + .model(&selection) + .factory(&factory) + .build(); + grid_view.connect_activate(move |_, position| { + let item = store.item(position).unwrap(); + let sticker: Sticker = item.downcast().unwrap(); + if let Some(filename) = sticker.filename() { + println!("{}", filename); + std::process::exit(0); + } + }); + sw.set_child(Some(&grid_view)); + + window.show(); + }); + + app.run(); +} diff --git a/tools/sticker-picker/src/sticker.rs b/tools/sticker-picker/src/sticker.rs new file mode 100644 index 00000000..7fb44e8e --- /dev/null +++ b/tools/sticker-picker/src/sticker.rs @@ -0,0 +1,106 @@ +// This file is part of Poezio. +// +// Poezio is free software: you can redistribute it and/or modify +// it under the terms of the zlib license. See the COPYING file. + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use std::cell::RefCell; +use std::path::Path; + +#[derive(Debug, Default)] +pub struct Sticker { + filename: RefCell>, + texture: RefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for Sticker { + const NAME: &'static str = "Sticker"; + type Type = StickerType; +} + +impl ObjectImpl for Sticker { + fn properties() -> &'static [glib::ParamSpec] { + use once_cell::sync::Lazy; + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecString::new( + "filename", + "Filename", + "Filename", + None, + glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, + ), + glib::ParamSpecObject::new( + "texture", + "Texture", + "Texture", + gdk::Texture::static_type(), + glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, + ), + ] + }); + PROPERTIES.as_ref() + } + + fn set_property( + &self, + _obj: &StickerType, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "filename" => { + let filename = value.get().unwrap(); + self.filename.replace(filename); + } + "texture" => { + let texture = value.get().unwrap(); + self.texture.replace(texture); + } + _ => unimplemented!(), + } + } + + fn property(&self, _obj: &StickerType, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "filename" => self.filename.borrow().to_value(), + "texture" => self.texture.borrow().to_value(), + _ => unimplemented!(), + } + } +} + +glib::wrapper! { + pub struct StickerType(ObjectSubclass); +} + +impl StickerType { + pub fn new(filename: String, path: &Path) -> StickerType { + let texture = gdk::Texture::from_filename(path).unwrap(); + glib::Object::new(&[("filename", &filename), ("texture", &texture)]) + .expect("Failed to create Sticker") + } + + pub fn filename(&self) -> Option { + let imp = self.imp(); + let filename = imp.filename.borrow(); + if let Some(filename) = filename.as_ref() { + Some(filename.clone()) + } else { + None + } + } + + pub fn texture(&self) -> Option { + let imp = self.imp(); + let texture = imp.texture.borrow(); + if let Some(texture) = texture.as_ref() { + Some(texture.clone()) + } else { + None + } + } +}