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;