summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MANIFEST.in1
-rw-r--r--doc/source/plugins/index.rst6
-rw-r--r--doc/source/plugins/sticker.rst6
-rw-r--r--plugins/sticker.py97
-rw-r--r--tools/sticker-picker/Cargo.toml16
-rw-r--r--tools/sticker-picker/src/main.rs93
-rw-r--r--tools/sticker-picker/src/sticker.rs106
7 files changed, 325 insertions, 0 deletions
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 <sticker-plugin>`
+
+ Opens a graphical sticker picker and sends the selected one.
+
Tell
:ref:`Documentation <tell-plugin>`
@@ -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 <pack>``
+
+ 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='<sticker pack>',
+ 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 <linkmauve@linkmauve.fr>"]
+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", &["<Control>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<Option<String>>,
+ texture: RefCell<Option<gdk::Texture>>,
+}
+
+#[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<Vec<glib::ParamSpec>> = 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<Sticker>);
+}
+
+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<String> {
+ 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<gdk::Texture> {
+ let imp = self.imp();
+ let texture = imp.texture.borrow();
+ if let Some(texture) = texture.as_ref() {
+ Some(texture.clone())
+ } else {
+ None
+ }
+ }
+}