1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//! Write HexChat plugins in Rust.
//!
//! To create your plugin:
//! - Make a library crate with [`crate-type = "cdylib"`](https://doc.rust-lang.org/cargo/reference/manifest.html#building-dynamic-or-static-libraries).
//! - Define a type, e.g. `struct MyPlugin`, to hold any state your plugin needs.
//! - Implement the [`Plugin`] trait for `MyPlugin`.
//! - Call [`export_plugin`] with the type `MyPlugin`, its name, description, and version.
//!
//! On Windows, it is recommended to add `-C target-feature=+crt-static` to your `RUSTFLAGS`,
//! for example in [`<project root>/.cargo/config`](https://doc.rust-lang.org/cargo/reference/config.html).
//! This ensures that your DLL does not dynamically import the MSVCRT.
//!
//! # Examples
//!
//! The following is a port of HexChat's [example](https://hexchat.readthedocs.io/en/latest/plugins.html#sample-plugin) "auto-op" plugin.
//! It will automatically OP everyone who joins (so don't try this if you're in a real channel!),
//! and can be toggled on and off with `/autooptoggle`.
//!
//! ```rust
//! use std::cell::Cell;
//! use hexavalent::{Plugin, PluginHandle, export_plugin};
//! use hexavalent::event::print::Join;
//! use hexavalent::hook::{Eat, Priority};
//! use hexavalent::str::HexStr;
//!
//! struct AutoOpPlugin {
//!     enabled: Cell<bool>,
//! }
//!
//! impl Default for AutoOpPlugin {
//!     fn default() -> Self {
//!         Self {
//!             enabled: Cell::new(true),
//!         }
//!     }
//! }
//!
//! impl AutoOpPlugin {
//!     fn autooptoggle_cb(&self, ph: PluginHandle<'_, Self>, _words: &[&HexStr]) -> Eat {
//!         if !self.enabled.get() {
//!             self.enabled.set(true);
//!             ph.print("Auto-Oping now enabled!");
//!         } else {
//!             self.enabled.set(false);
//!             ph.print("Auto-Oping now disabled!");
//!         }
//!         // eat this command so HexChat and other plugins can't process it
//!         Eat::All
//!     }
//!
//!     fn join_cb(&self, ph: PluginHandle<'_, Self>, args: [&HexStr; 4]) -> Eat {
//!         let [nick, _channel, _host, _account] = args;
//!         if self.enabled.get() {
//!             // op ANYONE who joins
//!             ph.command(format!("OP {}", nick));
//!         }
//!         // don't eat this event, HexChat needs to see it
//!         Eat::None
//!     }
//! }
//!
//! impl Plugin for AutoOpPlugin {
//!     fn init(&self, ph: PluginHandle<'_, Self>) {
//!         ph.hook_command(
//!             "AutoOpToggle",
//!             "Usage: AUTOOPTOGGLE, turns OFF/ON Auto-Oping",
//!             Priority::Normal,
//!             Self::autooptoggle_cb,
//!         );
//!         ph.hook_print(Join, Priority::Normal, Self::join_cb);
//!
//!         ph.print("AutoOpPlugin loaded successfully!");
//!     }
//!
//!     fn deinit(&self, ph: PluginHandle<'_, Self>) {
//!         ph.print("Unloading AutoOpPlugin...");
//!     }
//! }
//!
//! export_plugin!(AutoOpPlugin, "AutoOp", "Auto-Ops anyone who joins", "0.1");
//! ```
//!
//! # Safety
//!
//! In general, this library depends on HexChat invoking the plugin from only one thread.
//! If that is not the case, this library provides no guarantees.
//! (Although it is never explicitly stated that this is true, HexChat's plugin documentation says nothing of synchronization,
//! and none of the example plugins have any. It also seems true in practice.)
//!
//! In debug mode (specifically, when `debug_assertions` is enabled), the current thread ID is checked every time the plugin is invoked,
//! which can help detect misbehavior.

#![allow(
    clippy::get_first,
    clippy::result_unit_err,
    clippy::too_many_arguments,
    clippy::type_complexity
)]
#![warn(
    missing_debug_implementations,
    missing_docs,
    trivial_numeric_casts,
    unreachable_pub,
    unused_qualifications
)]
#![deny(unsafe_op_in_unsafe_fn)]

#[macro_use]
mod macros;

mod ffi;
mod iter;
mod plugin;
mod state;

#[doc(hidden)]
pub mod internal;

pub mod context;
pub mod event;
pub mod gui;
pub mod hook;
pub mod info;
pub mod list;
pub mod mode;
pub mod pref;
pub mod str;
pub mod strip;

pub use plugin::{Plugin, PluginHandle};

/// Defines the necessary exports for HexChat to load your plugin.
///
/// Do not define a `main` function; initialization should be performed in your plugin's [`Plugin::init`] function.
///
/// The type passed to `export_plugin` must implement [`Plugin`].
///
/// # Examples
///
/// ```rust
/// use hexavalent::{Plugin, PluginHandle, export_plugin};
///
/// #[derive(Default)]
/// struct NoopPlugin;
///
/// impl Plugin for NoopPlugin {
///     fn init(&self, ph: PluginHandle<'_, Self>) {
///         ph.print("Hello world!");
///     }
/// }
///
/// export_plugin!(NoopPlugin, "No-op", "Doesn't do anything", "1.0.0");
/// ```
///
/// Cargo's environment variables can also be used to copy `name`, `description`, and `version` from `Cargo.toml`.
///
/// ```rust
/// use hexavalent::{Plugin, PluginHandle, export_plugin};
///
/// #[derive(Default)]
/// struct NoopPlugin;
///
/// impl Plugin for NoopPlugin {
///     fn init(&self, ph: PluginHandle<'_, Self>) {
///         ph.print("Hello world!");
///     }
/// }
///
/// export_plugin!(
///     NoopPlugin,
///     env!("CARGO_PKG_NAME"),
///     env!("CARGO_PKG_DESCRIPTION"),
///     env!("CARGO_PKG_VERSION"),
/// );
/// ```
#[macro_export]
macro_rules! export_plugin {
    (
        $plugin_ty:ty,
        $name:expr,
        $desc:expr,
        $version:expr $(,)?
    ) => {
        #[no_mangle]
        pub unsafe extern "C" fn hexchat_plugin_init(
            plugin_handle: *mut $crate::internal::hexchat_plugin,
            plugin_name: *mut *const ::std::os::raw::c_char,
            plugin_desc: *mut *const ::std::os::raw::c_char,
            plugin_version: *mut *const ::std::os::raw::c_char,
            _arg: *mut ::std::os::raw::c_char,
        ) -> ::std::os::raw::c_int {
            const NAME: &::std::ffi::CStr =
                match ::std::ffi::CStr::from_bytes_with_nul(concat!($name, "\0").as_bytes()) {
                    Ok(x) => x,
                    Err(_) => unreachable!(),
                };
            const DESC: &::std::ffi::CStr =
                match ::std::ffi::CStr::from_bytes_with_nul(concat!($desc, "\0").as_bytes()) {
                    Ok(x) => x,
                    Err(_) => unreachable!(),
                };
            const VERSION: &::std::ffi::CStr =
                match ::std::ffi::CStr::from_bytes_with_nul(concat!($version, "\0").as_bytes()) {
                    Ok(x) => x,
                    Err(_) => unreachable!(),
                };

            // Safety: these literals are null-terminated and 'static
            *plugin_name = NAME.as_ptr();
            *plugin_desc = DESC.as_ptr();
            *plugin_version = VERSION.as_ptr();

            $crate::internal::hexchat_plugin_init::<$plugin_ty>(plugin_handle)
        }

        #[no_mangle]
        pub unsafe extern "C" fn hexchat_plugin_deinit(
            plugin_handle: *mut $crate::internal::hexchat_plugin,
        ) -> ::std::os::raw::c_int {
            $crate::internal::hexchat_plugin_deinit::<$plugin_ty>(plugin_handle)
        }
    };
}