weechat/hooks/
signal.rs

1use libc::{c_char, c_int};
2use std::{borrow::Cow, cell::Cell, ffi::CStr, marker::PhantomData, os::raw::c_void, ptr, rc::Rc};
3
4use weechat_sys::{t_gui_buffer, t_weechat_plugin};
5
6use super::Hook;
7use crate::{
8    buffer::{Buffer, InnerBuffer, InnerBuffers},
9    LossyCString, ReturnCode, Weechat,
10};
11
12/// Hook for a signal, the hook is removed when the object is dropped.
13pub struct SignalHook {
14    _hook: Hook,
15    _hook_data: Box<SignalHookData>,
16}
17
18struct SignalHookData {
19    callback: Box<dyn SignalCallback>,
20    weechat_ptr: *mut t_weechat_plugin,
21}
22
23/// Enum over the different data types a signal may send.
24#[non_exhaustive]
25pub enum SignalData<'a> {
26    /// String data
27    String(Cow<'a, str>),
28    /// Integer data
29    Integer(i32),
30    /// Buffer that was sent with the signal.
31    Buffer(Buffer<'a>),
32}
33
34impl<'a> Into<SignalData<'a>> for &'a str {
35    fn into(self) -> SignalData<'a> {
36        SignalData::String(Cow::from(self))
37    }
38}
39
40impl<'a> Into<SignalData<'a>> for String {
41    fn into(self) -> SignalData<'a> {
42        SignalData::String(Cow::from(self))
43    }
44}
45
46impl<'a> Into<SignalData<'a>> for i32 {
47    fn into(self) -> SignalData<'a> {
48        SignalData::Integer(self)
49    }
50}
51
52impl<'a> Into<SignalData<'a>> for Buffer<'a> {
53    fn into(self) -> SignalData<'a> {
54        SignalData::Buffer(self)
55    }
56}
57
58impl<'a> Into<SignalData<'a>> for &'a Buffer<'a> {
59    fn into(self) -> SignalData<'a> {
60        let ptr = self.ptr();
61
62        SignalData::Buffer(Buffer {
63            inner: InnerBuffers::BorrowedBuffer(InnerBuffer {
64                ptr,
65                weechat: self.inner.weechat_ptr(),
66                weechat_phantom: PhantomData,
67                closing: Rc::new(Cell::new(false)),
68            }),
69        })
70    }
71}
72
73impl<'a> SignalData<'a> {
74    fn pointer_is_buffer(signal_name: &str) -> bool {
75        // This table is taken from the Weechat plugin API docs
76        //
77        // https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_signal
78        match signal_name {
79            "irc_channel_opened" | "irc_pv_opened" | "irc_server_opened" => true,
80
81            "logger_start" | "logger_stop" | "logger_backlog" => true,
82
83            "spell_suggest" => true,
84
85            "buffer_opened" | "buffer_closing" | "buffer_closed" | "buffer_cleared" => true,
86
87            "buffer_filters_enabled"
88            | "buffer_filters_disabled"
89            | "buffer_hidden"
90            | "buffer_unhidden" => true,
91
92            "buffer_lines_hidden"
93            | "buffer_localvar_added"
94            | "buffer_localvar_changed"
95            | "buffer_localvar_removed"
96            | "buffer_merged"
97            | "buffer_unmerged"
98            | "buffer_moved"
99            | "buffer_renamed"
100            | "buffer_switch"
101            | "buffer_title_changed"
102            | "buffer_type_changed" => true,
103
104            "buffer_zoomed" | "buffer_unzoomed" => true,
105
106            "hotlist_changed" => true,
107
108            "input_search" | "input_text_changed" | "input_text_cursor_moved" => true,
109
110            // TODO nicklist group signals have a string representation of a
111            // pointer concatenated to the group name
112
113            // TODO some signals send out pointers to windows.
114            // TODO some signals send out pointers to infolists.
115            _ => false,
116        }
117    }
118
119    fn from_type_and_name(
120        weechat: &'a Weechat,
121        signal_name: &str,
122        data_type: &str,
123        data: *mut c_void,
124    ) -> Option<SignalData<'a>> {
125        // Some signals don't send any data, some other signals might send out a
126        // buffer pointer that might be null, in either case check if the
127        // pointer is valid first.
128        if data.is_null() {
129            return None;
130        }
131
132        match data_type {
133            "string" => unsafe {
134                Some(SignalData::String(
135                    CStr::from_ptr(data as *const c_char).to_string_lossy(),
136                ))
137            },
138            "integer" => {
139                let data = data as *const c_int;
140                unsafe { Some(SignalData::Integer(*(data))) }
141            }
142            "pointer" => {
143                if SignalData::pointer_is_buffer(signal_name) {
144                    Some(SignalData::Buffer(
145                        weechat.buffer_from_ptr(data as *mut t_gui_buffer),
146                    ))
147                } else {
148                    None
149                }
150            }
151            _ => None,
152        }
153    }
154}
155
156/// Trait for the signal callback.
157///
158/// A blanket implementation for pure `FnMut` functions exists, if data needs to
159/// be passed to the callback implement this over your struct.
160pub trait SignalCallback {
161    /// Callback that will be called when a signal is fired.
162    /// input field.
163    ///
164    /// # Arguments
165    ///
166    /// * `weechat` - A Weechat context.
167    ///
168    /// * `signal_name` - The name of the signal that fired the callback.
169    ///
170    /// * `data` - The data that was passed on by the signal.
171    fn callback(
172        &mut self,
173        weechat: &Weechat,
174        signal_name: &str,
175        data: Option<SignalData>,
176    ) -> ReturnCode;
177}
178
179impl<T: FnMut(&Weechat, &str, Option<SignalData>) -> ReturnCode + 'static> SignalCallback for T {
180    fn callback(
181        &mut self,
182        weechat: &Weechat,
183        signal_name: &str,
184        data: Option<SignalData>,
185    ) -> ReturnCode {
186        self(weechat, signal_name, data)
187    }
188}
189
190impl SignalHook {
191    /// Hook a signal.
192    ///
193    /// # Arguments
194    ///
195    /// * `signal_name` - The signal to hook (wildcard `*` is allowed).
196    ///
197    /// * `callback` - A function or a struct that implements SignalCallback,
198    /// the callback method of the trait will be called when the signal is
199    /// fired.
200    ///
201    /// # Panics
202    ///
203    /// Panics if the method is not called from the main Weechat thread.
204    ///
205    /// # Example
206    ///
207    /// ```no_run
208    /// # use weechat::{Weechat, ReturnCode};
209    /// # use weechat::hooks::{SignalData, SignalHook};
210    /// let signal_hook = SignalHook::new(
211    ///     "buffer_switch",
212    ///     |_weechat: &Weechat, _signal_name: &str, data: Option<SignalData>| {
213    ///         if let Some(data) = data {
214    ///             match data {
215    ///                 SignalData::Buffer(buffer) => {
216    ///                     buffer.print("Switched buffer")
217    ///                 }
218    ///                 _ => (),
219    ///             }
220    ///         }
221    ///
222    ///         ReturnCode::Ok
223    ///     },
224    /// );
225    ///
226    /// ```
227    pub fn new(signal_name: &str, callback: impl SignalCallback + 'static) -> Result<Self, ()> {
228        unsafe extern "C" fn c_hook_cb(
229            pointer: *const c_void,
230            _data: *mut c_void,
231            signal_name: *const c_char,
232            data_type: *const c_char,
233            signal_data: *mut c_void,
234        ) -> c_int {
235            let hook_data: &mut SignalHookData = { &mut *(pointer as *mut SignalHookData) };
236            let cb = &mut hook_data.callback;
237
238            let data_type = CStr::from_ptr(data_type).to_str().unwrap_or_default();
239            let signal_name = CStr::from_ptr(signal_name).to_str().unwrap_or_default();
240
241            let weechat = Weechat::from_ptr(hook_data.weechat_ptr);
242            let data =
243                SignalData::from_type_and_name(&weechat, signal_name, data_type, signal_data);
244
245            cb.callback(&weechat, signal_name, data) as i32
246        }
247
248        Weechat::check_thread();
249        let weechat = unsafe { Weechat::weechat() };
250
251        let data = Box::new(SignalHookData {
252            callback: Box::new(callback),
253            weechat_ptr: weechat.ptr,
254        });
255
256        let data_ref = Box::leak(data);
257        let hook_signal = weechat.get().hook_signal.unwrap();
258
259        let signal_name = LossyCString::new(signal_name);
260
261        let hook_ptr = unsafe {
262            hook_signal(
263                weechat.ptr,
264                signal_name.as_ptr(),
265                Some(c_hook_cb),
266                data_ref as *const _ as *const c_void,
267                ptr::null_mut(),
268            )
269        };
270
271        let hook_data = unsafe { Box::from_raw(data_ref) };
272        let hook = Hook {
273            ptr: hook_ptr,
274            weechat_ptr: weechat.ptr,
275        };
276
277        if hook_ptr.is_null() {
278            Err(())
279        } else {
280            Ok(SignalHook {
281                _hook: hook,
282                _hook_data: hook_data,
283            })
284        }
285    }
286}
287
288impl Weechat {
289    /// Send a signal.
290    ///
291    /// This will send out a signal and callbacks that are registered with a
292    /// `SignalHook` to listen to that signal wil get called.
293    ///
294    /// # Arguments
295    ///
296    /// * `signal_name` - The name of the signal that should be sent out. Common
297    ///     signals can be found in the Weechat plugin API [reference].
298    ///
299    /// * `data` - Data that should be provided to the signal callback. This can
300    ///     be a string, an i32 number, or a buffer.
301    ///
302    /// ```no_run
303    /// # use weechat::Weechat;
304    /// # use weechat::buffer::BufferBuilder;
305    /// # let buffer_handle = BufferBuilder::new("test")
306    /// #    .build()
307    /// #    .unwrap();
308    /// # let buffer = buffer_handle.upgrade().unwrap();
309    /// // Fetch the chat history for the buffer.
310    /// Weechat::hook_signal_send("logger_backlog", &buffer);
311    ///
312    /// // Signal that the input text changed.
313    /// Weechat::hook_signal_send("input_text_changed", "");
314    /// ```
315    ///
316    /// [reference]: https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_signal_send
317    pub fn hook_signal_send<'a, D: Into<SignalData<'a>>>(signal_name: &str, data: D) -> ReturnCode {
318        Weechat::check_thread();
319        let weechat = unsafe { Weechat::weechat() };
320
321        let signal_name = LossyCString::new(signal_name);
322        let signal_send = weechat.get().hook_signal_send.unwrap();
323        let data = data.into();
324
325        let ret = if let SignalData::String(string) = data {
326            let string = LossyCString::new(string);
327            unsafe {
328                signal_send(
329                    signal_name.as_ptr(),
330                    weechat_sys::WEECHAT_HOOK_SIGNAL_STRING as *const _ as *const i8,
331                    string.as_ptr() as *mut _,
332                )
333            }
334        } else {
335            let (ptr, data_type) = match data {
336                SignalData::Integer(number) => (
337                    number as *mut _,
338                    weechat_sys::WEECHAT_HOOK_SIGNAL_INT as *const u8,
339                ),
340                SignalData::Buffer(buffer) => (
341                    buffer.ptr() as *mut _,
342                    weechat_sys::WEECHAT_HOOK_SIGNAL_POINTER as *const u8,
343                ),
344                SignalData::String(_) => unreachable!(),
345            };
346            unsafe { signal_send(signal_name.as_ptr(), data_type as *const i8, ptr) }
347        };
348
349        match ret {
350            weechat_sys::WEECHAT_RC_OK => ReturnCode::Ok,
351            weechat_sys::WEECHAT_RC_OK_EAT => ReturnCode::OkEat,
352            weechat_sys::WEECHAT_RC_ERROR => ReturnCode::Error,
353            _ => ReturnCode::Error,
354        }
355    }
356}