zsh_module/lib.rs
1//! # Zsh Module
2//! This is a high level crate that allows you to define your own zsh module.
3//!
4//! ## Getting started
5//! To get started, first, you need to create library, not an executable. Then, change your crate
6//! type to `"cdylib"` on your `Cargo.toml`:
7//! ```toml
8//! [lib]
9//! crate-type = ["cdylib"]
10//! ```
11//!
12//! ## Boilerplate
13//! On your `lib.rs`, you need to put a [`export_module!`] macro call, alongside a `setup` function
14//! (can be called whatever you want):
15//! ```no_run
16//! use zsh_module::{ Module, ModuleBuilder };
17//!
18//! zsh_module::export_module!(my_module, setup);
19//!
20//! fn setup() -> Result<Module, Box<dyn std::error::Error>> {
21//! todo!()
22//! }
23//! ```
24//! ## The `setup` function
25//! A proper `setup` function must return a [`Result<Module, E>`] where `E` implements
26//! [`Error`][std::error::Error]. E.g:
27//! ```ignore
28//! fn setup() -> Result<Module, Box<dyn std::error::Error>> { .. }
29//!
30//! fn setup() -> Result<Module, anyhow::Error> { .. }
31//!
32//! fn setup() -> Result<Module, std::io::Error> { .. }
33//! ```
34//!
35//! ## Storing User Data
36//! You can store user data inside a module and have it accessible from any callbacks.
37//! Here's an example module, located at that defines a new `greet` builtin command:
38//! ```no_run
39//! use zsh_module::{Builtin, MaybeError, Module, ModuleBuilder, Opts};
40//!
41//! // Notice how this module gets installed as `rgreeter`
42//! zsh_module::export_module!(rgreeter, setup);
43//!
44//! struct Greeter;
45//!
46//! impl Greeter {
47//! fn greet_cmd(&mut self, _name: &str, _args: &[&str], _opts: Opts) -> MaybeError {
48//! println!("Hello, world!");
49//! Ok(())
50//! }
51//! }
52//!
53//! fn setup() -> Result<Module, Box<dyn std::error::Error>> {
54//! let module = ModuleBuilder::new(Greeter)
55//! .builtin(Greeter::greet_cmd, Builtin::new("greet"))
56//! .build();
57//! Ok(module)
58//! }
59//! ```
60//!
61//! ## Installing
62//! When your module is ready, copy your shared library to your distribution's zsh module folder
63//! and name it whatever you want, the only requirement is that it ends with your platforms's
64//! dynamic loadable library extension.
65//!
66//! On my machine, the zsh module folder is `/usr/lib/zsh/<zsh-version>/`.
67//!
68//! If everything went fine, you can load it in zsh using the following command:
69//! ```sh no_run
70//! zmodload <module-name>
71//! ```
72//!
73//! That is it!
74
75#![feature(trait_alias)]
76use std::{
77 any::Any,
78 borrow::Cow,
79 collections::HashMap,
80 error::Error,
81 ffi::{c_char, CStr, CString},
82};
83
84use features::Features;
85
86pub use options::Opts;
87use zsh_sys as zsys;
88
89mod features;
90mod hashtable;
91pub mod log;
92mod options;
93pub mod zsh;
94
95pub use hashtable::HashTable;
96
97/// A box error type for easier error handling.
98pub type AnyError = Box<dyn Error>;
99
100/// Represents the possibility of an error `E`.
101/// It is basically a [`Result`] that only cares for its [`Err`] variant.
102///
103/// # Generics
104/// You can (and should) replace the default error type `E` with your own [`Error`].
105pub type MaybeError<E = AnyError> = Result<(), E>;
106
107trait AnyCmd = Cmd<dyn Any, AnyError>;
108
109/// This trait corresponds to the function signature of a zsh builtin command handler.
110///
111/// # Generics
112/// - `A` is your User Data. For more info, read [`Storing User Data`]
113/// - `E` is anything that can be turned into a [`Box`]ed error.
114///
115/// # Example
116/// ```
117/// fn hello_cmd(data: &mut (), _cmd_name: &str, _args: &[&str], opts: zsh_module::Opts) -> zsh_module::MaybeError {
118/// println!("Hello, world!");
119/// Ok(())
120/// }
121/// ```
122///
123/// # See Also
124/// See [`ModuleBuilder::builtin`] for how to register a command.
125pub trait Cmd<A: Any + ?Sized, E: Into<AnyError>> =
126 'static + FnMut(&mut A, &str, &[&str], Opts) -> MaybeError<E>;
127
128pub(crate) fn to_cstr(string: impl Into<Vec<u8>>) -> CString {
129 CString::new(string).expect("Strings should not contain a null byte!")
130}
131
132/// Represents any type that can be represented as a C String. You shouldn't
133/// need to implement this yourself as the most commonly used `string`-y types
134/// already have this implemented.
135///
136/// # Examples
137/// ```
138/// use std::ffi::{CString, CStr};
139/// use std::borrow::Cow;
140///
141/// use zsh_module::ToCString;
142///
143/// let cstr = CStr::from_bytes_with_nul(b"Hello, world!\0").unwrap();
144/// let cstring = CString::new("Hello, world!").unwrap();
145///
146/// assert!(matches!(cstr.into_cstr(), Cow::Borrowed(data) if data == cstr));
147///
148/// let string = "Hello, world!";
149/// assert!(matches!(ToCString::into_cstr(string), Cow::Owned(cstring)));
150/// ```
151pub trait ToCString {
152 fn into_cstr<'a>(self) -> Cow<'a, CStr>
153 where
154 Self: 'a;
155}
156
157macro_rules! impl_tocstring {
158 ($($type:ty),*) => {
159 $(impl ToCString for $type {
160 fn into_cstr<'a>(self) -> Cow<'a, CStr> where Self: 'a {
161 Cow::Owned(to_cstr(self))
162 }
163 })*
164 };
165}
166
167impl_tocstring!(Vec<u8>, &[u8], &str, String);
168
169impl ToCString for &CStr {
170 fn into_cstr<'a>(self) -> Cow<'a, CStr>
171 where
172 Self: 'a,
173 {
174 Cow::Borrowed(self)
175 }
176}
177
178impl ToCString for CString {
179 fn into_cstr<'a>(self) -> Cow<'a, CStr> {
180 Cow::Owned(self)
181 }
182}
183
184impl ToCString for *const c_char {
185 fn into_cstr<'a>(self) -> Cow<'a, CStr> {
186 Cow::Borrowed(unsafe { CStr::from_ptr(self) })
187 }
188}
189
190impl ToCString for *mut c_char {
191 fn into_cstr<'a>(self) -> Cow<'a, CStr> {
192 Cow::Borrowed(unsafe { CStr::from_ptr(self) })
193 }
194}
195
196/// Properties of a zsh builtin command.
197///
198/// Any chages will reflect on the behaviour of the builtin
199pub struct Builtin {
200 minargs: i32,
201 maxargs: i32,
202 flags: Option<CString>,
203 name: CString,
204}
205
206impl Builtin {
207 /// Creates a command builtin.
208 ///
209 /// By default, the builtin can take any amount of arguments (minargs and maxargs are 0 and
210 /// [`None`], respectively) and no flags.
211 pub fn new(name: &str) -> Self {
212 Self {
213 minargs: 0,
214 maxargs: -1,
215 flags: None,
216 name: to_cstr(name),
217 }
218 }
219 /// Sets the minimum amount of arguments allowed by the builtin
220 pub fn minargs(mut self, value: i32) -> Self {
221 self.minargs = value;
222 self
223 }
224 /// Sets the maximum amount of arguments allowed by the builtin
225 pub fn maxargs(mut self, value: Option<u32>) -> Self {
226 self.maxargs = value.map(|i| i as i32).unwrap_or(-1);
227 self
228 }
229 /// Sets flags recognized by the builtin
230 pub fn flags(mut self, value: &str) -> Self {
231 self.flags = Some(to_cstr(value));
232 self
233 }
234}
235
236type Bintable = HashMap<Box<CStr>, Box<dyn AnyCmd>>;
237
238/// Allows you to build a [`Module`]
239pub struct ModuleBuilder<A> {
240 user_data: A,
241 binaries: Vec<zsys::builtin>,
242 bintable: Bintable,
243 strings: Vec<Box<CStr>>,
244}
245
246impl<A> ModuleBuilder<A>
247where
248 A: Any + 'static,
249{
250 //! Creates an empty [`Self`] with options ready for configuration.
251 pub fn new(user_data: A) -> Self {
252 Self {
253 user_data,
254 binaries: vec![],
255 bintable: HashMap::new(),
256 strings: Vec::with_capacity(8),
257 }
258 }
259 /// Registers a new builtin command
260 pub fn builtin<E, C>(self, mut cb: C, builtin: Builtin) -> Self
261 where
262 E: Into<Box<dyn Error>>,
263 C: Cmd<A, E>,
264 {
265 let closure: Box<dyn AnyCmd> = Box::new(
266 move |data: &mut (dyn Any + 'static), name, args, opts| -> MaybeError<AnyError> {
267 cb(data.downcast_mut::<A>().unwrap(), name, args, opts).map_err(E::into)
268 },
269 );
270 self.add_builtin(
271 builtin.name,
272 builtin.minargs,
273 builtin.maxargs,
274 builtin.flags,
275 closure,
276 )
277 }
278 fn hold_cstring(&mut self, value: impl Into<Vec<u8>>) -> *mut i8 {
279 let value = to_cstr(value).into_boxed_c_str();
280 let ptr = value.as_ptr();
281 self.strings.push(value);
282 ptr as *mut _
283 }
284 fn add_builtin(
285 mut self,
286 name: CString,
287 minargs: i32,
288 maxargs: i32,
289 options: Option<CString>,
290 cb: Box<dyn AnyCmd + 'static>,
291 ) -> Self {
292 let name = name.into_boxed_c_str();
293 let flags = match options {
294 Some(flags) => self.hold_cstring(flags),
295 None => std::ptr::null_mut(),
296 };
297 let raw = zsys::builtin {
298 node: zsys::hashnode {
299 next: std::ptr::null_mut(),
300 nam: name.as_ptr() as *mut _,
301 // !TODO: add flags param
302 flags: 0,
303 },
304 // The handler function will be set later by the zsh module glue
305 handlerfunc: None,
306 minargs,
307 maxargs,
308 funcid: 0,
309 optstr: flags,
310 defopts: std::ptr::null_mut(),
311 };
312 self.binaries.push(raw);
313 self.bintable.insert(name, cb);
314 self
315 }
316 /// Creates a new module, ready to be used.
317 pub fn build(self) -> Module {
318 Module::new(self)
319 }
320}
321
322/// Hooks into the Zsh module system and connects it to your `User Data`.
323pub struct Module {
324 user_data: Box<dyn Any>,
325 features: Features,
326 bintable: Bintable,
327 #[allow(dead_code)]
328 strings: Vec<Box<CStr>>,
329 name: Option<&'static str>,
330}
331
332impl Module {
333 fn new<A: Any + 'static>(desc: ModuleBuilder<A>) -> Self {
334 let features = Features::empty().binaries(desc.binaries.into());
335 Self {
336 user_data: Box::new(desc.user_data),
337 features,
338 bintable: desc.bintable,
339 strings: desc.strings,
340 name: None,
341 }
342 }
343}
344
345#[cfg(feature = "export_module")]
346#[doc(hidden)]
347pub mod export_module;