├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md └── crates ├── weechat-macro ├── Cargo.toml ├── LICENSE.md ├── README.md └── src │ └── lib.rs ├── weechat-sys ├── Cargo.toml ├── LICENSE.md ├── README.md ├── build.rs └── src │ ├── lib.rs │ ├── weechat-plugin.h │ └── wrapper.h └── weechat ├── Cargo.toml ├── LICENSE.md ├── README.md ├── build.rs ├── examples ├── go │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ └── lib.rs ├── grep │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── buffer.rs │ │ └── lib.rs ├── infolist │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ └── lib.rs └── sample │ ├── .rustfmt.toml │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ └── lib.rs └── src ├── buffer ├── lines.rs ├── mod.rs ├── nick.rs ├── nickgroup.rs └── window.rs ├── config ├── boolean.rs ├── color.rs ├── config.rs ├── config_options.rs ├── enum.rs ├── integer.rs ├── mod.rs ├── section.rs └── string.rs ├── config_macros.rs ├── executor.rs ├── hashtable.rs ├── hdata.rs ├── hooks ├── bar.rs ├── commands.rs ├── completion.rs ├── fd.rs ├── mod.rs ├── modifier.rs ├── signal.rs └── timer.rs ├── infolist.rs ├── lib.rs └── weechat.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.yml] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ main ] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | style: 13 | name: Check style 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout the repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Install rust 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: nightly 24 | components: rustfmt 25 | 26 | - name: Cargo fmt 27 | run: cargo fmt --all -- --check 28 | 29 | typos: 30 | name: Spell Check with Typos 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - name: Checkout Actions Repository 35 | uses: actions/checkout@v4 36 | 37 | - name: Check the spelling of the files in our repo 38 | uses: crate-ci/typos@master 39 | 40 | clippy: 41 | name: Run clippy 42 | needs: [style] 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Checkout the repo 47 | uses: actions/checkout@v4 48 | 49 | - name: Install WeeChat 50 | run: | 51 | sudo mkdir /root/.gnupg 52 | sudo chmod 700 /root/.gnupg 53 | sudo mkdir -p /usr/share/keyrings 54 | sudo gpg --no-default-keyring --keyring /usr/share/keyrings/weechat-archive-keyring.gpg --keyserver hkps://keys.openpgp.org --recv-keys 11E9DE8848F2B65222AA75B8D1820DB22A11534E 55 | echo "deb [signed-by=/usr/share/keyrings/weechat-archive-keyring.gpg] https://weechat.org/ubuntu jammy main" | sudo tee /etc/apt/sources.list.d/weechat.list 56 | echo "deb-src [signed-by=/usr/share/keyrings/weechat-archive-keyring.gpg] https://weechat.org/ubuntu jammy main" | sudo tee -a /etc/apt/sources.list.d/weechat.list 57 | sudo apt-get update 58 | sudo apt-get install weechat-dev 59 | 60 | - name: Install rust 61 | uses: dtolnay/rust-toolchain@master 62 | with: 63 | toolchain: nightly 64 | components: clippy 65 | 66 | - uses: Swatinem/rust-cache@v2 67 | 68 | - name: Clippy 69 | run: cargo clippy --all-targets --all-features -- -D warnings 70 | 71 | test: 72 | name: ${{ matrix.target.name }} ${{ matrix.channel }} 73 | needs: [clippy] 74 | 75 | runs-on: ${{ matrix.target.os }} 76 | strategy: 77 | matrix: 78 | target: [ 79 | { "os": "ubuntu-latest", "toolchain": "x86_64-unknown-linux-gnu", "name": "Linux GNU (64-bit)" }, 80 | { "os": "ubuntu-latest", "toolchain": "i686-unknown-linux-gnu", "name": "Linux GNU (32-bit)" }, 81 | # TODO: Add some more OS variants here. 82 | ] 83 | channel: [stable, beta, nightly] 84 | 85 | steps: 86 | - name: Checkout 87 | uses: actions/checkout@v4 88 | 89 | - name: Install WeeChat 90 | if: runner.os == 'Linux' 91 | run: | 92 | sudo mkdir /root/.gnupg 93 | sudo chmod 700 /root/.gnupg 94 | sudo mkdir -p /usr/share/keyrings 95 | sudo gpg --no-default-keyring --keyring /usr/share/keyrings/weechat-archive-keyring.gpg --keyserver hkps://keys.openpgp.org --recv-keys 11E9DE8848F2B65222AA75B8D1820DB22A11534E 96 | echo "deb [signed-by=/usr/share/keyrings/weechat-archive-keyring.gpg] https://weechat.org/ubuntu jammy main" | sudo tee /etc/apt/sources.list.d/weechat.list 97 | echo "deb-src [signed-by=/usr/share/keyrings/weechat-archive-keyring.gpg] https://weechat.org/ubuntu jammy main" | sudo tee -a /etc/apt/sources.list.d/weechat.list 98 | sudo apt-get update 99 | sudo apt-get install weechat-dev 100 | 101 | - if: ${{ matrix.target.toolchain == 'i686-unknown-linux-gnu' }} 102 | run: sudo apt-get install libc6-dev-i386 103 | 104 | - run: rustup target add ${{ matrix.target.toolchain }} 105 | 106 | - name: Install rust 107 | uses: dtolnay/rust-toolchain@master 108 | with: 109 | toolchain: ${{ matrix.channel }} 110 | targets: ${{ matrix.target.toolchain }} 111 | 112 | - uses: Swatinem/rust-cache@v2 113 | 114 | - name: Test 115 | run: cargo test --all-features --target ${{ matrix.target.toolchain }} 116 | 117 | test-api: 118 | name: Test Weechat API 119 | needs: [test] 120 | runs-on: ubuntu-latest 121 | strategy: 122 | fail-fast: false 123 | matrix: 124 | toolchain: 125 | - x86_64-unknown-linux-gnu 126 | - i686-unknown-linux-gnu 127 | version: 128 | - v3.8 129 | - v4.0.0 130 | - v4.1.0 131 | - v4.2.0 132 | - v4.3.0 133 | - v4.4.0 134 | - v4.5.0 135 | - v4.6.0 136 | env: 137 | WEECHAT_BUNDLED: 'no' 138 | WEECHAT_PLUGIN_FILE: '${{ github.workspace }}/weechat-src/src/plugins/weechat-plugin.h' 139 | steps: 140 | - name: Checkout 141 | uses: actions/checkout@v4 142 | 143 | - name: Checkout weechat API header 144 | uses: actions/checkout@v4 145 | with: 146 | repository: 'weechat/weechat' 147 | ref: ${{ matrix.version }} 148 | path: 'weechat-src' 149 | sparse-checkout: src/plugins/weechat-plugin.h 150 | sparse-checkout-cone-mode: false 151 | 152 | - run: sudo apt -y update 153 | - run: sudo apt -y install libclang-dev 154 | 155 | - if: ${{ matrix.toolchain == 'i686-unknown-linux-gnu' }} 156 | run: sudo apt-get install libc6-dev-i386 157 | 158 | - run: rustup target add ${{ matrix.toolchain }} 159 | 160 | - name: Install rust 161 | uses: dtolnay/rust-toolchain@master 162 | with: 163 | targets: ${{ matrix.toolchain }} 164 | toolchain: stable 165 | 166 | - uses: Swatinem/rust-cache@v2 167 | 168 | - name: Test 169 | run: cargo test --all-features --target ${{ matrix.toolchain }} 170 | 171 | - name: Build 172 | run: cargo build --all-features --target ${{ matrix.toolchain }} 173 | 174 | - name: Lint 175 | run: cargo clippy --all-features --target ${{ matrix.toolchain }} 176 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | !.gitignore 4 | .ccls-cache 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | max_width = 100 3 | comment_width = 80 4 | wrap_comments = true 5 | imports_granularity = "Crate" 6 | use_small_heuristics = "Max" 7 | group_imports = "StdExternalCrate" 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.command": "clippy", 3 | "rust-analyzer.cargo.features": "all", 4 | "rust-analyzer.rustfmt": { 5 | "extraArgs": ["+nightly"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | ] 5 | resolver = "2" 6 | 7 | [profile.release] 8 | opt-level = "z" 9 | codegen-units = 1 10 | lto = true 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 rust-weechat contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/poljar/rust-weechat/actions/workflows/ci.yml/badge.svg)](https://github.com/poljar/rust-weechat/actions/workflows/ci.yml) 2 | [![Docs](https://docs.rs/weechat/badge.svg)](https://docs.rs/weechat) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | # rust-weechat 6 | 7 | **rust-weechat** is a high level wrapper of the [Weechat][] plugin API in [Rust][]. 8 | 9 | [Weechat]: https://weechat.org/ 10 | [Rust]: https://www.rust-lang.org/ 11 | 12 | ## Project structure 13 | 14 | The project consists of three different crates. 15 | 16 | - **weechat** - High level and idiomatic Rust library that allows to easily 17 | write [Weechat][] plugins. 18 | - **weechat-macro** - Procedural macro implementations that allow you to define 19 | C entry functions that a Weechat plugin needs to define. 20 | - **weechat-sys** - Auto-generated low level bindings to the Weechat plugin API. 21 | 22 | ## Status 23 | 24 | The library is in an beta state, things that are implemented generally work some 25 | breaking changes might still be introduced. A lot of the less used plugin APIs 26 | aren't yet implemented. 27 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # hsignal 2 | 3 | hsignal sends out hashmaps, hashmaps can contain ints, strings, 4 | pointer, buffers, or time_t values as keys as well as values, but no mixing is 5 | allowed. 6 | 7 | We can check the type of the keys/values using `hashtable_get_string()` and map them 8 | to the `SignalData` enum. 9 | 10 | # hsignal_send 11 | 12 | Turn a rust hashmap into a weechat one. We will need to use generics here since 13 | only one type at a time can be used. Eg Hashmap<&str, &str>, Hashmap<&str, Buffer>, 14 | so the second argument of the hashmap needs to be generic. 15 | 16 | # key_bind 17 | 18 | Create a builder for keybinds, allow adding multiple `.add_bind()` that takes 19 | two strings. Build a hashmap out of this and convert it into a weechat hashmap. 20 | 21 | # macro for commands 22 | 23 | ????? 24 | -------------------------------------------------------------------------------- /crates/weechat-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat-macro" 3 | version = "0.4.0" 4 | authors = ["Damir Jelić "] 5 | description = "Procedural macros for the weechat crate" 6 | edition = "2018" 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/poljar/rust-weechat" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | libc = "0.2.132" 16 | syn = "1.0.99" 17 | proc-macro2 = "1.0.43" 18 | quote = "1.0.21" 19 | 20 | [dev-dependencies] 21 | weechat = { path = "../weechat" } 22 | -------------------------------------------------------------------------------- /crates/weechat-macro/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rust-weechat contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/weechat-macro/README.md: -------------------------------------------------------------------------------- 1 | # weechat-macro 2 | 3 | Macro to define a [Weechat] [plugin] in a declarative way. 4 | 5 | This library is supposed to be used through the 6 | [weechat](https://crates.io/crates/weechat) crate. 7 | 8 | [Weechat]: weechat.org/ 9 | [plugin]: https://weechat.org/files/doc/stable/weechat_plugin_api.en.html 10 | -------------------------------------------------------------------------------- /crates/weechat-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "256"] 2 | 3 | extern crate proc_macro; 4 | use std::collections::HashMap; 5 | 6 | use proc_macro2::{Ident, Literal}; 7 | use quote::quote; 8 | use syn::{ 9 | parse::{Parse, ParseStream, Result}, 10 | parse_macro_input, 11 | punctuated::Punctuated, 12 | Error, LitStr, 13 | }; 14 | 15 | struct WeechatPluginInfo { 16 | plugin: syn::Ident, 17 | name: (usize, Literal), 18 | author: (usize, Literal), 19 | description: (usize, Literal), 20 | version: (usize, Literal), 21 | license: (usize, Literal), 22 | } 23 | 24 | enum WeechatVariable { 25 | Name(syn::LitStr), 26 | Author(syn::LitStr), 27 | Description(syn::LitStr), 28 | Version(syn::LitStr), 29 | License(syn::LitStr), 30 | } 31 | 32 | impl WeechatVariable { 33 | #[allow(clippy::wrong_self_convention)] 34 | fn to_pair(string: &LitStr) -> (usize, Literal) { 35 | let mut bytes = string.value().into_bytes(); 36 | // Push a null byte since this goes to the C side. 37 | bytes.push(0); 38 | 39 | (bytes.len(), Literal::byte_string(&bytes)) 40 | } 41 | 42 | fn as_pair(&self) -> (usize, Literal) { 43 | match self { 44 | WeechatVariable::Name(string) => WeechatVariable::to_pair(string), 45 | WeechatVariable::Author(string) => WeechatVariable::to_pair(string), 46 | WeechatVariable::Description(string) => WeechatVariable::to_pair(string), 47 | WeechatVariable::Version(string) => WeechatVariable::to_pair(string), 48 | WeechatVariable::License(string) => WeechatVariable::to_pair(string), 49 | } 50 | } 51 | 52 | fn default_literal() -> (usize, Literal) { 53 | let bytes = vec![0]; 54 | (bytes.len(), Literal::byte_string(&bytes)) 55 | } 56 | } 57 | 58 | impl Parse for WeechatVariable { 59 | fn parse(input: ParseStream) -> Result { 60 | let key: Ident = input.parse()?; 61 | input.parse::()?; 62 | let value = input.parse()?; 63 | 64 | match key.to_string().to_lowercase().as_ref() { 65 | "name" => Ok(WeechatVariable::Name(value)), 66 | "author" => Ok(WeechatVariable::Author(value)), 67 | "description" => Ok(WeechatVariable::Description(value)), 68 | "version" => Ok(WeechatVariable::Version(value)), 69 | "license" => Ok(WeechatVariable::License(value)), 70 | _ => Err(Error::new( 71 | key.span(), 72 | "expected one of name, author, description, version or license", 73 | )), 74 | } 75 | } 76 | } 77 | 78 | impl Parse for WeechatPluginInfo { 79 | fn parse(input: ParseStream) -> Result { 80 | let plugin: syn::Ident = input.parse().map_err(|_e| { 81 | Error::new(input.span(), "a struct that implements the Plugin trait needs to be given") 82 | })?; 83 | input.parse::()?; 84 | 85 | let args: Punctuated = 86 | input.parse_terminated(WeechatVariable::parse)?; 87 | let mut variables = HashMap::new(); 88 | 89 | for arg in args.pairs() { 90 | let variable = arg.value(); 91 | match variable { 92 | WeechatVariable::Name(_) => variables.insert("name", *variable), 93 | WeechatVariable::Author(_) => variables.insert("author", *variable), 94 | WeechatVariable::Description(_) => variables.insert("description", *variable), 95 | WeechatVariable::Version(_) => variables.insert("version", *variable), 96 | WeechatVariable::License(_) => variables.insert("license", *variable), 97 | }; 98 | } 99 | 100 | Ok(WeechatPluginInfo { 101 | plugin, 102 | name: variables.remove("name").map_or_else( 103 | || Err(Error::new(input.span(), "the name of the plugin needs to be defined")), 104 | |v| Ok(v.as_pair()), 105 | )?, 106 | author: variables 107 | .remove("author") 108 | .map_or_else(WeechatVariable::default_literal, |v| v.as_pair()), 109 | description: variables 110 | .remove("description") 111 | .map_or_else(WeechatVariable::default_literal, |v| v.as_pair()), 112 | version: variables 113 | .remove("version") 114 | .map_or_else(WeechatVariable::default_literal, |v| v.as_pair()), 115 | license: variables 116 | .remove("license") 117 | .map_or_else(WeechatVariable::default_literal, |v| v.as_pair()), 118 | }) 119 | } 120 | } 121 | 122 | /// Register a struct that implements the `Plugin` trait as a Weechat plugin. 123 | /// 124 | /// This configures the Weechat init and end method as well as additional plugin 125 | /// metadata. 126 | /// 127 | /// # Example 128 | /// ``` 129 | /// # use weechat::{plugin, Args, Weechat, Plugin}; 130 | /// # struct SamplePlugin; 131 | /// # impl Plugin for SamplePlugin { 132 | /// # fn init(weechat: &Weechat, _args: Args) -> Result { 133 | /// # Ok(SamplePlugin) 134 | /// # } 135 | /// # } 136 | /// plugin!( 137 | /// SamplePlugin, 138 | /// name: "rust_sample", 139 | /// author: "poljar", 140 | /// description: "", 141 | /// version: "0.1.0", 142 | /// license: "MIT" 143 | /// ); 144 | /// ``` 145 | #[proc_macro] 146 | pub fn plugin(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 147 | let WeechatPluginInfo { plugin, name, author, description, version, license } = 148 | parse_macro_input!(input as WeechatPluginInfo); 149 | 150 | let (name_len, name) = name; 151 | let (author_len, author) = author; 152 | let (description_len, description) = description; 153 | let (license_len, license) = license; 154 | let (version_len, version) = version; 155 | 156 | let result = quote! { 157 | #[doc(hidden)] 158 | #[no_mangle] 159 | pub static weechat_plugin_api_version: [u8; weechat::weechat_sys::WEECHAT_PLUGIN_API_VERSION_LENGTH] = 160 | *weechat::weechat_sys::WEECHAT_PLUGIN_API_VERSION; 161 | 162 | #[doc(hidden)] 163 | #[no_mangle] 164 | pub static weechat_plugin_name: [u8; #name_len] = *#name; 165 | 166 | #[doc(hidden)] 167 | #[no_mangle] 168 | pub static weechat_plugin_author: [u8; #author_len] = *#author; 169 | 170 | #[doc(hidden)] 171 | #[no_mangle] 172 | pub static weechat_plugin_description: [u8; #description_len] = *#description; 173 | 174 | #[doc(hidden)] 175 | #[no_mangle] 176 | pub static weechat_plugin_version: [u8; #version_len] = *#version; 177 | 178 | #[doc(hidden)] 179 | #[no_mangle] 180 | pub static weechat_plugin_license: [u8; #license_len] = *#license; 181 | 182 | #[doc(hidden)] 183 | static mut __PLUGIN: Option<#plugin> = None; 184 | 185 | /// This function is called when plugin is loaded by WeeChat. 186 | /// 187 | /// # Safety 188 | /// This function needs to be an extern C function and it can't be 189 | /// mangled, otherwise Weechat will not find the symbol. 190 | #[doc(hidden)] 191 | #[no_mangle] 192 | pub unsafe extern "C" fn weechat_plugin_init( 193 | plugin: *mut weechat::weechat_sys::t_weechat_plugin, 194 | argc: weechat::libc::c_int, 195 | argv: *mut *mut weechat::libc::c_char, 196 | ) -> weechat::libc::c_int { 197 | let weechat = unsafe { 198 | Weechat::init_from_ptr(plugin) 199 | }; 200 | 201 | let args = unsafe { Args::new(argc, argv) }; 202 | 203 | match <#plugin as ::weechat::Plugin>::init(&weechat, args) { 204 | Ok(p) => { 205 | unsafe { 206 | __PLUGIN = Some(p); 207 | } 208 | return weechat::weechat_sys::WEECHAT_RC_OK; 209 | } 210 | Err(_e) => { 211 | return weechat::weechat_sys::WEECHAT_RC_ERROR; 212 | } 213 | } 214 | } 215 | 216 | /// This function is called when plugin is unloaded by WeeChat. 217 | /// 218 | /// # Safety 219 | /// This function needs to be an extern C function and it can't be 220 | /// mangled, otherwise Weechat will not find the symbol. 221 | #[doc(hidden)] 222 | #[no_mangle] 223 | pub unsafe extern "C" fn weechat_plugin_end( 224 | _plugin: *mut weechat::weechat_sys::t_weechat_plugin 225 | ) -> weechat::libc::c_int { 226 | unsafe { 227 | __PLUGIN = None; 228 | Weechat::free(); 229 | } 230 | weechat::weechat_sys::WEECHAT_RC_OK 231 | } 232 | 233 | impl #plugin { 234 | /// Get a reference to our created plugin. 235 | /// 236 | /// # Panic 237 | /// 238 | /// Panics if this is called before the plugin `init()` method is 239 | /// done. 240 | pub fn get() -> &'static mut #plugin { 241 | unsafe { 242 | match &mut __PLUGIN { 243 | Some(p) => p, 244 | None => panic!("Weechat plugin isn't initialized"), 245 | } 246 | } 247 | } 248 | } 249 | }; 250 | 251 | result.into() 252 | } 253 | -------------------------------------------------------------------------------- /crates/weechat-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat-sys" 3 | version = "0.4.0" 4 | authors = ["Damir Jelić "] 5 | description = "Rust low level API bindings for Weechat" 6 | edition = "2018" 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/poljar/rust-weechat" 10 | 11 | [build-dependencies] 12 | bindgen = "0.70.0" 13 | 14 | [dependencies] 15 | libc = "0.2.132" 16 | -------------------------------------------------------------------------------- /crates/weechat-sys/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rust-weechat contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/weechat-sys/README.md: -------------------------------------------------------------------------------- 1 | # weechat-sys 2 | 3 | Auto-generated bindings for the [Weechat] plugin [API] 4 | 5 | This library needs the `weechat-plugin.h` include file. It will try to use the 6 | system-wide installed one. If it can't find that one it will use a bundled one. 7 | 8 | ## Choosing the plugin include file. 9 | 10 | By default the system-wide include file will be used if found, this behaviour 11 | can be overridden with two environment flags. 12 | 13 | To prefer the bundled include file `WEECHAT_BUNDLED` should be set to `true`. 14 | 15 | A custom include file can be set with the `WEECHAT_PLUGIN_FILE` environment 16 | variable, this environment variable takes a full path to the custom include 17 | file. 18 | 19 | [Weechat]: weechat.org/ 20 | [API]: https://weechat.org/files/doc/stable/weechat_plugin_api.en.html 21 | -------------------------------------------------------------------------------- /crates/weechat-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | use bindgen::{BindgenError, Bindings}; 4 | 5 | const WEECHAT_BUNDLED_ENV: &str = "WEECHAT_BUNDLED"; 6 | const WEECHAT_PLUGIN_FILE_ENV: &str = "WEECHAT_PLUGIN_FILE"; 7 | 8 | fn build(file: &str) -> Result { 9 | const INCLUDED_TYPES: &[&str] = &[ 10 | "t_weechat_plugin", 11 | "t_gui_buffer", 12 | "t_gui_nick", 13 | "t_gui_nick_group", 14 | "t_hook", 15 | "t_hdata", 16 | ]; 17 | const INCLUDED_VARS: &[&str] = &[ 18 | "WEECHAT_PLUGIN_API_VERSION", 19 | "WEECHAT_HASHTABLE_INTEGER", 20 | "WEECHAT_HASHTABLE_STRING", 21 | "WEECHAT_HASHTABLE_POINTER", 22 | "WEECHAT_HASHTABLE_BUFFER", 23 | "WEECHAT_HASHTABLE_TIME", 24 | "WEECHAT_HOOK_SIGNAL_STRING", 25 | "WEECHAT_HOOK_SIGNAL_INT", 26 | "WEECHAT_HOOK_SIGNAL_POINTER", 27 | ]; 28 | let mut builder = bindgen::Builder::default(); 29 | 30 | builder = builder.header(file); 31 | 32 | for t in INCLUDED_TYPES { 33 | builder = builder.allowlist_type(t); 34 | } 35 | 36 | for v in INCLUDED_VARS { 37 | builder = builder.allowlist_var(v); 38 | } 39 | 40 | builder.generate() 41 | } 42 | 43 | fn main() { 44 | let bundled = 45 | env::var(WEECHAT_BUNDLED_ENV).is_ok_and(|bundled| match bundled.to_lowercase().as_ref() { 46 | "1" | "true" | "yes" => true, 47 | "0" | "false" | "no" => false, 48 | _ => panic!("Invalid value for WEECHAT_BUNDLED, must be true/false"), 49 | }); 50 | 51 | let plugin_file = env::var(WEECHAT_PLUGIN_FILE_ENV); 52 | 53 | let bindings = if bundled { 54 | println!("cargo::warning=Using vendored header"); 55 | build("src/weechat-plugin.h").expect("Unable to generate bindings") 56 | } else { 57 | match plugin_file { 58 | Ok(file) => { 59 | let path = PathBuf::from(file).canonicalize().expect("Can't canonicalize path"); 60 | println!("cargo::warning=Using system header"); 61 | build(path.to_str().unwrap_or_default()).unwrap_or_else(|_| { 62 | panic!("Unable to generate bindings with the provided {:?}", path) 63 | }) 64 | } 65 | Err(_) => { 66 | println!("cargo::warning=Using system header via wrapper.h"); 67 | let bindings = build("src/wrapper.h"); 68 | 69 | match bindings { 70 | Ok(b) => b, 71 | Err(_) => build("src/weechat-plugin.h").expect("Unable to generate bindings"), 72 | } 73 | } 74 | } 75 | }; 76 | 77 | println!("cargo:rerun-if-env-changed={WEECHAT_BUNDLED_ENV}"); 78 | println!("cargo:rerun-if-env-changed={WEECHAT_PLUGIN_FILE_ENV}"); 79 | 80 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 81 | bindings.write_to_file(out_path.join("bindings.rs")).expect("Couldn't write bindings!"); 82 | } 83 | -------------------------------------------------------------------------------- /crates/weechat-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(clippy::type_complexity)] 5 | #![allow(clippy::redundant_static_lifetimes)] 6 | 7 | use libc::c_int; 8 | 9 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 10 | 11 | pub const WEECHAT_PLUGIN_API_VERSION_LENGTH: usize = 12; 12 | 13 | /* return codes for plugin functions */ 14 | pub const WEECHAT_RC_OK: c_int = 0; 15 | pub const WEECHAT_RC_OK_EAT: c_int = 1; 16 | pub const WEECHAT_RC_ERROR: c_int = -1; 17 | 18 | pub const WEECHAT_CONFIG_OPTION_SET_OK_CHANGED: c_int = 2; 19 | pub const WEECHAT_CONFIG_OPTION_SET_OK_SAME_VALUE: c_int = 1; 20 | pub const WEECHAT_CONFIG_OPTION_SET_ERROR: c_int = 0; 21 | pub const WEECHAT_CONFIG_OPTION_SET_OPTION_NOT_FOUND: c_int = -1; 22 | 23 | pub const WEECHAT_CONFIG_READ_OK: c_int = 0; 24 | pub const WEECHAT_CONFIG_READ_MEMORY_ERROR: c_int = -1; 25 | pub const WEECHAT_CONFIG_READ_FILE_NOT_FOUND: c_int = -2; 26 | -------------------------------------------------------------------------------- /crates/weechat-sys/src/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /crates/weechat/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat" 3 | version = "0.4.0" 4 | authors = ["Damir Jelić "] 5 | 6 | repository = "https://github.com/poljar/rust-weechat/" 7 | homepage = "https://github.com/poljar/rust-weechat/" 8 | edition = "2018" 9 | 10 | readme = "README.md" 11 | description = "Weechat API bindings for Rust" 12 | license = "MIT" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["docs"] 16 | rustdoc-args = ["--cfg", "feature=\"docs\""] 17 | 18 | [features] 19 | # Support to run futures on the Weechat main thread. 20 | async = ["async-task", "pipe-channel", "futures", "async-trait"] 21 | 22 | # Declarative configuration macro. 23 | config_macro = ["paste", "strum"] 24 | 25 | # Still unsound or experimental features will be hidden behind this flag. 26 | unsound = [] 27 | 28 | docs = ["async", "unsound", "config_macro"] 29 | 30 | [dependencies] 31 | libc = "0.2.132" 32 | 33 | backtrace = "0.3.66" 34 | async-task = { version = "4.3.0", optional = true } 35 | async-trait = { version = "0.1.57", optional = true } 36 | pipe-channel = { version = "1.3.0", optional = true } 37 | futures = { version = "0.3.24", optional = true } 38 | paste = { version = "1.0.9", optional = true } 39 | strum = { version = "0.24.1", optional = true } 40 | 41 | weechat-macro = { version = "0.4.0", path = "../weechat-macro" } 42 | weechat-sys = { version = "0.4.0", path = "../weechat-sys" } 43 | 44 | [dev-dependencies] 45 | async-std = "1.12.0" 46 | pipe-channel = "1.3.0" 47 | strum = "0.24.1" 48 | strum_macros = "0.24.3" 49 | futures = "0.3.24" 50 | 51 | [build-dependencies] 52 | weechat-sys = { version = "0.4.0", path = "../weechat-sys" } 53 | -------------------------------------------------------------------------------- /crates/weechat/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rust-weechat contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/weechat/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/poljar/rust-weechat.svg?branch=master)](https://travis-ci.org/poljar/rust-weechat) 2 | [![Docs](https://docs.rs/weechat/badge.svg)](https://docs.rs/weechat) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | # Rust-Weechat 6 | 7 | [Weechat](https://weechat.org/) is an extensible chat client. 8 | 9 | [Rust-Weechat](https://github.com/poljar/rust-weechat/) is a high level Rust 10 | library providing an API for building Weechat plugins. 11 | 12 | It wraps the Weechat C plugin [API] as safe Rust bindings. 13 | 14 | ## Project Status 15 | 16 | This project is in a decently stable state, many things that the Weechat plugin 17 | API allows are exposed in a higher level safe API. Many things still need to be 18 | figured out and exposed safely. Breaking changes might still get introduced. 19 | 20 | Experimental or unsound features are gated behind feature flags. 21 | 22 | ## Example 23 | 24 | Example plugins can be found in the [examples] part of the repository. 25 | 26 | The following example shows a minimal working Rust plugin. 27 | 28 | ```rust 29 | use weechat::{ 30 | buffer::Buffer, 31 | weechat_plugin, Args, Weechat, Plugin, 32 | }; 33 | 34 | struct HelloWorld; 35 | 36 | impl Plugin for HelloWorld { 37 | fn init(_: &Weechat, _: Args) -> Result { 38 | Weechat::print("Hello from Rust"); 39 | Ok(Self) 40 | } 41 | } 42 | 43 | impl Drop for HelloWorld { 44 | fn drop(&mut self) { 45 | Weechat::print("Bye from Rust"); 46 | } 47 | } 48 | 49 | weechat_plugin!( 50 | HelloWorld, 51 | name: "hello", 52 | author: "Damir Jelić ", 53 | description: "Simple hello world Rust plugin", 54 | version: "1.0.0", 55 | license: "MIT" 56 | ); 57 | ``` 58 | 59 | ## Projects build with Rust-Weechat 60 | 61 | * [Weechat-Matrix-rs](https://github.com/poljar/weechat-matrix-rs) 62 | * [Weechat-Discord](https://github.com/terminal-discord/weechat-discord) 63 | 64 | Are we missing a project? Submit a pull request and we'll get you added! 65 | Just edit this `README.md` file. 66 | 67 | ## Picking the correct Weechat version. 68 | 69 | By default the system-wide `weechat-plugin.h` file will be used if found, 70 | this behaviour can be overridden with two environment flags. 71 | 72 | To prefer a bundled include file `WEECHAT_BUNDLED` should be set to `true`. The 73 | bundled include file tracks the latest Weechat release. 74 | 75 | A custom include file can be set with the `WEECHAT_PLUGIN_FILE` environment 76 | variable, this environment variable takes a full path to the include file. 77 | 78 | [Weechat]: weechat.org/ 79 | [API]: https://weechat.org/files/doc/stable/weechat_plugin_api.en.html 80 | [examples]: https://github.com/poljar/rust-weechat/tree/master/weechat/examples 81 | -------------------------------------------------------------------------------- /crates/weechat/build.rs: -------------------------------------------------------------------------------- 1 | /// Keep only versions that break signatures in crate 2 | #[allow(unused)] 3 | #[repr(u64)] 4 | enum WeechatApiVersions { 5 | V4_1_0 = 20230908, 6 | V4_2_0 = 20240105, 7 | } 8 | 9 | fn main() { 10 | println!("cargo::rerun-if-changed=build.rs"); 11 | println!("cargo::rerun-if-env-changed=WEECHAT_BUNDLED"); 12 | println!("cargo::rerun-if-env-changed=WEECHAT_PLUGIN_FILE"); 13 | println!("cargo::rustc-check-cfg=cfg(weechat410)"); 14 | println!("cargo::rustc-check-cfg=cfg(weechat420)"); 15 | 16 | let (version, _) = std::str::from_utf8(weechat_sys::WEECHAT_PLUGIN_API_VERSION) 17 | .expect("Failed to parse weechat version string") 18 | .split_once('-') 19 | .expect("Failed to split weechat version string"); 20 | 21 | println!("cargo::warning=WEECHAT_PLUGIN_API_VERSION: {version}"); 22 | 23 | let version: u64 = version.parse().expect("Failed to parse weechat version string as u64"); 24 | 25 | use crate::WeechatApiVersions::*; 26 | match version { 27 | v if v >= V4_2_0 as _ => { 28 | println!("cargo::rustc-cfg=weechat420"); 29 | } 30 | v if v < V4_2_0 as _ => { 31 | println!("cargo::rustc-cfg=weechat410"); 32 | } 33 | _ => { 34 | println!("cargo::error=Failed to match weechat API version: {version}"); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/weechat/examples/go/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat-go" 3 | version = "0.1.0" 4 | authors = ["Damir Jelić "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "go" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | fuzzy-matcher = "0.3.7" 13 | 14 | [dependencies.weechat] 15 | path = "../../" 16 | features = ["config_macro", "unsound"] 17 | -------------------------------------------------------------------------------- /crates/weechat/examples/go/Makefile: -------------------------------------------------------------------------------- 1 | WEECHAT_HOME ?= $(HOME)/.weechat 2 | PREFIX ?= $(WEECHAT_HOME) 3 | 4 | .PHONY: install install-dir lint 5 | 6 | target/debug/libgo.so: src/lib.rs 7 | cargo build 8 | 9 | install: install-dir target/debug/libgo.so 10 | install -m644 target/debug/libgo.so $(DESTDIR)$(PREFIX)/plugins/go.so 11 | 12 | install-dir: 13 | install -d $(DESTDIR)$(PREFIX)/plugins 14 | 15 | lint: 16 | cargo clippy 17 | -------------------------------------------------------------------------------- /crates/weechat/examples/go/README.md: -------------------------------------------------------------------------------- 1 | # go 2 | 3 | Weechat go reimplementation in rust. 4 | 5 | This is a port of the popular Python [go script] for Weechat. It uses a fuzzy 6 | matching library to match buffers by their short name. 7 | 8 | ## Build 9 | 10 | To build the plugin 11 | ``` 12 | make 13 | ``` 14 | 15 | Installation can be done like so 16 | 17 | ``` 18 | make install 19 | ``` 20 | 21 | By default this will install the plugin in your `$HOME/.weechat/plugins` directory. 22 | 23 | ### Picking the correct Weechat version. 24 | 25 | By default the system-wide `weechat-plugin.h` file will be used if found, 26 | this behaviour can be overridden with two environment flags. 27 | 28 | To prefer a bundled include file `WEECHAT_BUNDLED` should be set to `true`. The 29 | bundled include file tracks the latest Weechat release. 30 | 31 | A custom include file can be set with the `WEECHAT_PLUGIN_FILE` environment 32 | variable, this environment variable takes a full path to the include file. 33 | 34 | After an adequate `weechat-plugin.h` file is found rebuild the plugin like so 35 | 36 | ``` 37 | WEECHAT_PLUGIN_FILE=/home/example/weechat-plugin.h make install 38 | ``` 39 | 40 | [go script]: https://weechat.org/scripts/source/go.py.html/ 41 | -------------------------------------------------------------------------------- /crates/weechat/examples/grep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat-ripgrep" 3 | version = "0.1.0" 4 | authors = ["Damir Jelić "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "ripgrep" 9 | crate-type = ["dylib"] 10 | 11 | [dependencies] 12 | grep-searcher = "0.1.7" 13 | grep-matcher = "0.1.4" 14 | grep-regex = "0.1.8" 15 | 16 | tokio = { version = "1.4.0", features = [ "rt-multi-thread", "sync" ] } 17 | async-trait = "0.1.48" 18 | 19 | [dependencies.weechat] 20 | path = "../../" 21 | features = ["async"] 22 | 23 | [dependencies.clap] 24 | version = "2.33.3" 25 | default-features = false 26 | features = [ "suggestions" ] 27 | -------------------------------------------------------------------------------- /crates/weechat/examples/grep/Makefile: -------------------------------------------------------------------------------- 1 | WEECHAT_HOME ?= $(HOME)/.weechat 2 | PREFIX ?= $(WEECHAT_HOME) 3 | SOURCES = src/lib.rs src/buffer.rs 4 | 5 | .PHONY: format install install-dir 6 | 7 | target/debug/libripgrep.so: $(SOURCES) 8 | cargo build 9 | 10 | install: target/debug/libripgrep.so install-dir 11 | install -m644 target/debug/libripgrep.so $(DESTDIR)$(PREFIX)/plugins/ripgrep.so 12 | 13 | install-dir: 14 | install -d $(DESTDIR)$(PREFIX)/plugins 15 | 16 | format: 17 | cargo fmt 18 | -------------------------------------------------------------------------------- /crates/weechat/examples/grep/README.md: -------------------------------------------------------------------------------- 1 | # grep 2 | 3 | Weechat grep reimplementation in rust. 4 | 5 | This is a port of the popular Python [grep script] for Weechat. It uses ripgrep 6 | to provide a fast search experience. 7 | 8 | ## Build 9 | 10 | To build the plugin 11 | ``` 12 | make 13 | ``` 14 | 15 | Installation can be done like so 16 | 17 | ``` 18 | make install 19 | ``` 20 | 21 | By default this will install the plugin in your `$HOME/.weechat/plugins` directory. 22 | 23 | ### Picking the correct Weechat version. 24 | 25 | By default the system-wide `weechat-plugin.h` file will be used if found, 26 | this behaviour can be overridden with two environment flags. 27 | 28 | To prefer a bundled include file `WEECHAT_BUNDLED` should be set to `true`. The 29 | bundled include file tracks the latest Weechat release. 30 | 31 | A custom include file can be set with the `WEECHAT_PLUGIN_FILE` environment 32 | variable, this environment variable takes a full path to the include file. 33 | 34 | After an adequate `weechat-plugin.h` file is found rebuild the plugin like so 35 | 36 | ``` 37 | WEECHAT_PLUGIN_FILE=/home/example/weechat-plugin.h make install 38 | ``` 39 | 40 | [grep script]: https://weechat.org/scripts/source/grep.py.html/ 41 | -------------------------------------------------------------------------------- /crates/weechat/examples/grep/src/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::RipgrepCommand; 2 | use std::{path::Path, time::Duration}; 3 | use weechat::{ 4 | buffer::{BufferBuilder, BufferHandle}, 5 | Weechat, 6 | }; 7 | 8 | pub struct GrepBuffer { 9 | buffer: BufferHandle, 10 | } 11 | 12 | impl GrepBuffer { 13 | pub fn new(command: &RipgrepCommand) -> GrepBuffer { 14 | let buffer_handle = BufferBuilder::new("ripgrep") 15 | .close_callback(command.clone()) 16 | .input_callback(command.clone()) 17 | .build() 18 | .expect("Can't create ripgrep buffer"); 19 | 20 | let buffer = buffer_handle.upgrade().unwrap(); 21 | 22 | buffer.disable_nicklist(); 23 | buffer.disable_time_for_each_line(); 24 | buffer.disable_log(); 25 | buffer.set_title("ripgrep output buffer"); 26 | 27 | GrepBuffer { 28 | buffer: buffer_handle, 29 | } 30 | } 31 | 32 | fn split_line(line: &str) -> (&str, &str, String) { 33 | let tab_count = line.matches('\t').count(); 34 | 35 | let (date, nick, msg) = if tab_count >= 2 { 36 | let vec: Vec<&str> = line.splitn(3, '\t').collect(); 37 | (vec[0], vec[1], vec[2]) 38 | } else { 39 | ("", "", line) 40 | }; 41 | 42 | let msg = msg.trim().replace("\t", " "); 43 | (date.trim(), nick.trim(), msg) 44 | } 45 | 46 | fn format_line(&self, line: &str) -> String { 47 | let (date, nick, msg) = GrepBuffer::split_line(line); 48 | let nick = self.colorize_nick(nick); 49 | 50 | format!( 51 | "{date_color}{date}{reset} {nick} {msg}", 52 | date_color = Weechat::color("brown"), 53 | date = date, 54 | reset = Weechat::color("reset"), 55 | nick = nick, 56 | msg = msg 57 | ) 58 | } 59 | 60 | fn print(&self, line: &str) { 61 | self.buffer 62 | .upgrade() 63 | .unwrap() 64 | .print(&self.format_line(line)); 65 | } 66 | 67 | fn colorize_nick(&self, nick: &str) -> String { 68 | if nick.is_empty() { 69 | return "".to_owned(); 70 | } 71 | 72 | // TODO colorize the nick prefix and suffix 73 | // TODO handle the extra nick prefix and suffix settings 74 | 75 | let (prefix, nick) = { 76 | let first_char = nick.chars().next(); 77 | match first_char { 78 | Some('&') | Some('@') | Some('!') | Some('+') | Some('%') => { 79 | (first_char, &nick[1..]) 80 | } 81 | Some(_) => (None, nick), 82 | None => (None, nick), 83 | } 84 | }; 85 | 86 | let prefix = match prefix { 87 | Some(p) => p.to_string(), 88 | None => "".to_owned(), 89 | }; 90 | 91 | let nick_color = Weechat::info_get("nick_color_name", nick).unwrap(); 92 | 93 | format!( 94 | "{}{}{}{}", 95 | prefix, 96 | Weechat::color(&nick_color), 97 | nick, 98 | Weechat::color("reset") 99 | ) 100 | } 101 | 102 | fn print_status(&self, line: &str) { 103 | self.buffer.upgrade().unwrap().print(&format!( 104 | "{}[{}grep{}]{}\t{}", 105 | Weechat::color("chat_delimiters"), 106 | Weechat::color("chat_nick"), 107 | Weechat::color("chat_delimiters"), 108 | Weechat::color("reset"), 109 | line 110 | )) 111 | } 112 | 113 | fn set_title(&self, title: &str) { 114 | self.buffer.upgrade().unwrap().set_title(title); 115 | } 116 | 117 | pub fn switch_to(&self) { 118 | self.buffer.upgrade().unwrap().switch_to(); 119 | } 120 | 121 | pub fn print_result( 122 | &self, 123 | search_term: &str, 124 | file: &Path, 125 | duration: Duration, 126 | result: &[String], 127 | ) { 128 | self.print_status(&format!( 129 | "{summary_color}Search for {emph_color}{pattern}{summary_color} \ 130 | in {emph_color}{file:?}{color_reset}.", 131 | summary_color = Weechat::color("cyan"), 132 | emph_color = Weechat::color("lightcyan"), 133 | color_reset = Weechat::color("reset"), 134 | pattern = search_term, 135 | file = file 136 | )); 137 | 138 | let max_lines = std::cmp::min(result.len(), 4000); 139 | 140 | for line in &result[..max_lines] { 141 | self.print(&line); 142 | } 143 | 144 | self.print_status(&format!( 145 | "{summary_color}{matches} matches \"{emph_color}{search_term}\ 146 | {summary_color}\" in {emph_color}{file:?}{color_reset}.", 147 | summary_color = Weechat::color("cyan"), 148 | emph_color = Weechat::color("lightcyan"), 149 | matches = result.len(), 150 | search_term = search_term, 151 | file = file, 152 | color_reset = Weechat::color("reset") 153 | )); 154 | 155 | let title = format!( 156 | "'q': close buffer | Search in {color_title}{file:?}{color_reset} \ 157 | {matches} matches | pattern \"{color_title}{search_term}{color_reset}\" \ 158 | | {duration:?}", 159 | color_title = Weechat::color("yellow"), 160 | file = file, 161 | color_reset = Weechat::color("reset"), 162 | matches = result.len(), 163 | search_term = search_term, 164 | duration = duration, 165 | ); 166 | 167 | self.set_title(&title); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /crates/weechat/examples/grep/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod buffer; 2 | 3 | use clap::{App, Arg}; 4 | use std::{ 5 | borrow::Cow, 6 | path::{Path, PathBuf}, 7 | str::FromStr, 8 | }; 9 | use tokio::{ 10 | runtime::{Builder, Runtime}, 11 | sync::mpsc::{channel, Receiver, Sender}, 12 | }; 13 | 14 | use grep_regex::RegexMatcher; 15 | use grep_searcher::{sinks::Lossy, Searcher}; 16 | use std::io; 17 | 18 | use std::{cell::RefCell, rc::Rc, time::Instant}; 19 | 20 | use weechat::{infolist::InfolistVariable, Args, Plugin, Weechat}; 21 | 22 | use weechat::{ 23 | buffer::{Buffer, BufferCloseCallback, BufferInputCallback}, 24 | config::{BooleanOptionSettings, Config, ConfigOption, ConfigSectionSettings}, 25 | hooks::{Command, CommandCallback, CommandSettings}, 26 | plugin, Prefix, 27 | }; 28 | 29 | use buffer::GrepBuffer; 30 | 31 | type SearchResult = Result, io::Error>; 32 | 33 | struct Ripgrep { 34 | _config: Rc>, 35 | _command: Command, 36 | _runtime: Rc>>, 37 | } 38 | 39 | #[derive(Clone)] 40 | pub struct RipgrepCommand { 41 | config: Rc>, 42 | buffer: Rc>>, 43 | runtime: Rc>>, 44 | last_search_file: Rc>>, 45 | } 46 | 47 | impl RipgrepCommand { 48 | /// Wait for the result from the search task and print it out. 49 | /// 50 | /// This runs on the main Weechat thread. 51 | // TODO we could spawn this task from the search task running on the Tokio 52 | // runtime using Weechat::spawn_from_thread(). This would get rid of the 53 | // receiver. 54 | async fn receive_result( 55 | &self, 56 | file: PathBuf, 57 | search_term: String, 58 | mut receiver: Receiver, 59 | ) { 60 | let start = Instant::now(); 61 | let result = receiver.recv().await; 62 | 63 | let result = if let Some(result) = result { 64 | match result { 65 | Ok(r) => r, 66 | Err(e) => { 67 | Weechat::print(&format!("Error searching: {:?}", e)); 68 | return; 69 | } 70 | } 71 | } else { 72 | Weechat::print("Error searching: empty result"); 73 | return; 74 | }; 75 | 76 | self.last_search_file.borrow_mut().replace(file.clone()); 77 | 78 | let buffer = &self.buffer; 79 | let buffer_exists = buffer.borrow().is_some(); 80 | 81 | if !buffer_exists { 82 | let buffer_handle = GrepBuffer::new(&self); 83 | *buffer.borrow_mut() = Some(buffer_handle); 84 | } 85 | 86 | let buffer_borrow = buffer.borrow(); 87 | let buffer = buffer_borrow.as_ref().expect("Buffer wasn't created"); 88 | 89 | let end = Instant::now(); 90 | 91 | buffer.print_result(&search_term, &file, end - start, &result); 92 | 93 | let config = self.config.borrow(); 94 | let section = config.search_section("main").unwrap(); 95 | let go_to_buffer = section.search_option("go_to_buffer").unwrap(); 96 | 97 | let go_to_buffer = match go_to_buffer { 98 | ConfigOption::Boolean(opt) => opt, 99 | _ => panic!("Invalid option type"), 100 | }; 101 | 102 | if go_to_buffer.value() { 103 | buffer.switch_to(); 104 | } 105 | } 106 | 107 | /// Helper to spawn a result receiving coroutine, so we avoid using async 108 | /// coroutines which are not stable. 109 | async fn receive_result_helper( 110 | command: RipgrepCommand, 111 | file: PathBuf, 112 | search_term: String, 113 | rx: Receiver, 114 | ) { 115 | command.receive_result(file, search_term, rx).await 116 | } 117 | 118 | /// Get the logger file for the given buffer from the infolist. 119 | fn file_from_infolist(&self, weechat: &Weechat, buffer: &Buffer) -> Option { 120 | let infolist = weechat.get_infolist("logger_buffer", None).ok()?; 121 | 122 | for item in infolist { 123 | let info_buffer = if let Some(b) = item.get("buffer") { 124 | b 125 | } else { 126 | continue; 127 | }; 128 | 129 | if let InfolistVariable::Buffer(info_buffer) = info_buffer { 130 | if buffer == &info_buffer { 131 | let path = item.get("log_filename")?; 132 | 133 | if let InfolistVariable::String(path) = path { 134 | return Some(path.to_string()); 135 | } 136 | } 137 | } 138 | } 139 | 140 | None 141 | } 142 | 143 | /// Guess the log file from the buffer name. 144 | fn file_from_name(&self, full_name: &str) -> PathBuf { 145 | let weechat_home = Weechat::info_get("weechat_dir", "").expect("Can't find Weechat home"); 146 | let mut file = Path::new(&weechat_home).join("logs"); 147 | let mut full_name = full_name.to_owned(); 148 | full_name.push_str(".weechatlog"); 149 | file.push(full_name); 150 | file 151 | } 152 | 153 | /// Get the log file for a buffer. 154 | fn get_file_by_buffer(&self, weechat: &Weechat, buffer: &Buffer) -> Option { 155 | let path = self.file_from_infolist(weechat, buffer); 156 | 157 | if let Some(path) = path { 158 | PathBuf::from_str(&path) 159 | } else { 160 | let full_name = buffer.full_name().to_lowercase(); 161 | Ok(self.file_from_name(&full_name)) 162 | } 163 | .ok() 164 | } 165 | 166 | /// Search the given file using the given regex matcher. 167 | /// 168 | /// This runs on the Tokio executor in a separate thread, returns the 169 | /// searchresult through a mpsc channel to the Weechat thread. 170 | async fn search(file: PathBuf, matcher: RegexMatcher, sender: Sender) { 171 | let mut matches: Vec = vec![]; 172 | 173 | let sink = Lossy(|_, line| { 174 | matches.push(line.to_string()); 175 | Ok(true) 176 | }); 177 | 178 | match Searcher::new().search_path(&matcher, file, sink) { 179 | Ok(_) => sender.send(Ok(matches)), 180 | Err(e) => sender.send(Err(e)), 181 | } 182 | .await 183 | .unwrap_or(()); 184 | } 185 | 186 | /// Start a search. 187 | /// 188 | /// This spawns a Tokio task to search the given file and a Weechat task to 189 | /// wait for the result. 190 | fn start_search(&self, term: &str, file: &Path) { 191 | let matcher = match RegexMatcher::new(term) { 192 | Ok(m) => m, 193 | Err(e) => { 194 | Weechat::print(&format!( 195 | "{} Invalid regular expression {:?}", 196 | Weechat::prefix(Prefix::Error), 197 | e 198 | )); 199 | return; 200 | } 201 | }; 202 | 203 | let (tx, rx) = channel(1); 204 | 205 | self.runtime 206 | .borrow_mut() 207 | .as_ref() 208 | .unwrap() 209 | .spawn(RipgrepCommand::search(file.to_owned(), matcher, tx)); 210 | Weechat::spawn(RipgrepCommand::receive_result_helper( 211 | self.clone(), 212 | file.to_owned(), 213 | term.to_string(), 214 | rx, 215 | )) 216 | .detach(); 217 | } 218 | } 219 | 220 | impl BufferInputCallback for RipgrepCommand { 221 | fn callback(&mut self, _weechat: &Weechat, buffer: &Buffer, input: Cow) -> Result<(), ()> { 222 | if input == "q" || input == "Q" { 223 | buffer.close(); 224 | return Ok(()); 225 | } 226 | 227 | let file = self.last_search_file.borrow(); 228 | 229 | let file = match &*file { 230 | Some(f) => f, 231 | None => return Err(()), 232 | }; 233 | 234 | self.start_search(&input, file); 235 | 236 | Ok(()) 237 | } 238 | } 239 | 240 | impl BufferCloseCallback for RipgrepCommand { 241 | fn callback(&mut self, _weechat: &Weechat, _buffer: &Buffer) -> Result<(), ()> { 242 | self.buffer.borrow_mut().take(); 243 | Ok(()) 244 | } 245 | } 246 | 247 | impl CommandCallback for RipgrepCommand { 248 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer, arguments: Args) { 249 | let parsed_args = App::new("rg") 250 | .arg( 251 | Arg::with_name("pattern") 252 | .index(1) 253 | .value_name("PATTERN") 254 | .help("A regular expression used for searching.") 255 | .multiple(true), 256 | ) 257 | .get_matches_from_safe(arguments); 258 | 259 | let parsed_args = match parsed_args { 260 | Ok(a) => a, 261 | Err(e) => { 262 | Weechat::print(&format!("Error parsing grep args {}", e)); 263 | return; 264 | } 265 | }; 266 | 267 | let file = self.get_file_by_buffer(weechat, buffer); 268 | 269 | let file = match file { 270 | Some(f) => f, 271 | None => return, 272 | }; 273 | 274 | let pattern = match parsed_args.value_of("pattern") { 275 | Some(p) => p, 276 | None => { 277 | Weechat::print("Invalid pattern"); 278 | return; 279 | } 280 | }; 281 | 282 | self.start_search(pattern, &file); 283 | } 284 | } 285 | 286 | impl Plugin for Ripgrep { 287 | fn init(_: &Weechat, _args: Args) -> Result { 288 | let mut config = Config::new("ripgrep")?; 289 | 290 | { 291 | let section_settings = ConfigSectionSettings::new("main"); 292 | let mut section = config 293 | .new_section(section_settings) 294 | .expect("Can't create main config section"); 295 | 296 | let option_settings = BooleanOptionSettings::new("go_to_buffer") 297 | .description("Automatically go to grep buffer when search is over.") 298 | .default_value(true); 299 | 300 | section 301 | .new_boolean_option(option_settings) 302 | .expect("Can't create boolean option"); 303 | } 304 | 305 | let config = Rc::new(RefCell::new(config)); 306 | 307 | let command_info = CommandSettings::new("rg"); 308 | 309 | let runtime = Builder::new_multi_thread() 310 | .worker_threads(4) 311 | .thread_name("ripgrep-searcher") 312 | .build() 313 | .expect("Can't create the Tokio runtime"); 314 | 315 | let runtime = Rc::new(RefCell::new(Some(runtime))); 316 | 317 | let command = Command::new( 318 | command_info, 319 | RipgrepCommand { 320 | runtime: runtime.clone(), 321 | buffer: Rc::new(RefCell::new(None)), 322 | config: config.clone(), 323 | last_search_file: Rc::new(RefCell::new(None)), 324 | }, 325 | ); 326 | 327 | Ok(Ripgrep { 328 | _config: config, 329 | _command: command?, 330 | _runtime: runtime, 331 | }) 332 | } 333 | } 334 | 335 | plugin!( 336 | Ripgrep, 337 | name: "ripgrep", 338 | author: "Damir Jelic ", 339 | description: "Search in buffers and logs using ripgrep", 340 | version: "0.1.0", 341 | license: "ISC" 342 | ); 343 | -------------------------------------------------------------------------------- /crates/weechat/examples/infolist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat-infolist" 3 | version = "0.1.0" 4 | authors = ["Damir Jelić "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "infolist" 9 | crate-type = ["cdylib"] 10 | 11 | 12 | [dependencies] 13 | chrono = "0.4.19" 14 | itertools = "0.10.0" 15 | 16 | [dependencies.weechat] 17 | path = "../../" 18 | -------------------------------------------------------------------------------- /crates/weechat/examples/infolist/Makefile: -------------------------------------------------------------------------------- 1 | WEECHAT_HOME ?= $(HOME)/.weechat 2 | PREFIX ?= $(WEECHAT_HOME) 3 | 4 | .PHONY: install install-dir lint 5 | 6 | target/debug/libinfolist.so: src/lib.rs 7 | cargo build 8 | 9 | install: install-dir target/debug/libinfolist.so 10 | install -m644 target/debug/libinfolist.so $(DESTDIR)$(PREFIX)/plugins/infolist.so 11 | 12 | install-dir: 13 | install -d $(DESTDIR)$(PREFIX)/plugins 14 | 15 | lint: 16 | cargo clippy 17 | -------------------------------------------------------------------------------- /crates/weechat/examples/infolist/README.md: -------------------------------------------------------------------------------- 1 | # infolist 2 | 3 | Weechat infolist reimplementation in Rust. 4 | 5 | This is a port of the Python [infolist script] for Weechat. 6 | 7 | ## Build 8 | 9 | To build the plugin 10 | ``` 11 | make 12 | ``` 13 | 14 | Installation can be done like so 15 | 16 | ``` 17 | make install 18 | ``` 19 | 20 | By default this will install the plugin in your `$HOME/.weechat/plugins` directory. 21 | 22 | ### Picking the correct Weechat version. 23 | 24 | By default the system-wide `weechat-plugin.h` file will be used if found, 25 | this behaviour can be overridden with two environment flags. 26 | 27 | To prefer a bundled include file `WEECHAT_BUNDLED` should be set to `true`. The 28 | bundled include file tracks the latest Weechat release. 29 | 30 | A custom include file can be set with the `WEECHAT_PLUGIN_FILE` environment 31 | variable, this environment variable takes a full path to the include file. 32 | 33 | After an adequate `weechat-plugin.h` file is found rebuild the plugin like so 34 | 35 | ``` 36 | WEECHAT_PLUGIN_FILE=/home/example/weechat-plugin.h make install 37 | ``` 38 | 39 | [infolist script]: https://weechat.org/scripts/source/infolist.py.html/ 40 | -------------------------------------------------------------------------------- /crates/weechat/examples/infolist/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2008-2018 Sébastien Helleu 2 | // Copyright (C) 2020 Damir Jelić 3 | // 4 | // This program is free software; you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation; either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::{borrow::Cow, cell::RefCell, rc::Rc}; 18 | 19 | use chrono::{offset::Utc, DateTime}; 20 | use itertools::sorted; 21 | 22 | use weechat::{ 23 | buffer::{Buffer, BufferBuilder, BufferCloseCallback, BufferHandle, BufferInputCallback}, 24 | hooks::{Command, CommandCallback, CommandSettings}, 25 | infolist::InfolistVariable, 26 | plugin, Args, Plugin, Prefix, Weechat, 27 | }; 28 | 29 | #[allow(unused)] 30 | struct Infolist { 31 | command: Command, 32 | } 33 | 34 | #[derive(Default, Clone)] 35 | struct InnerInfolist { 36 | buffer: Rc>>, 37 | } 38 | 39 | impl InnerInfolist { 40 | fn set_title(&self, weechat: &Weechat, buffer: &Buffer) { 41 | let infolist = weechat 42 | .get_infolist("hook", Some("infolist")) 43 | .expect("Can't get the infolist list"); 44 | 45 | let infolist_names: Vec = infolist 46 | .filter_map(|item| { 47 | let name = item.get("infolist_name")?; 48 | if let InfolistVariable::String(n) = name { 49 | Some(n.to_string()) 50 | } else { 51 | None 52 | } 53 | }) 54 | .collect(); 55 | 56 | buffer.set_title(&format!( 57 | "Infolist 0.1 | Infolists: {}", 58 | infolist_names.join(" ") 59 | )); 60 | } 61 | 62 | fn display_infolist(&self, weechat: &Weechat, buffer: &Buffer, args: &str) { 63 | let mut args = args.splitn(2, ' '); 64 | 65 | let infolist_name = args.next().unwrap_or_default(); 66 | let infolist_args = args.next(); 67 | 68 | let infolist = if let Ok(i) = weechat.get_infolist(infolist_name, infolist_args) { 69 | i 70 | } else { 71 | buffer.print(&format!( 72 | "{}Infolist {} not found", 73 | Weechat::prefix(Prefix::Error), 74 | infolist_name 75 | )); 76 | return; 77 | }; 78 | 79 | buffer.clear(); 80 | buffer.print_date_tags( 81 | 0, 82 | &["no_filter"], 83 | &format!( 84 | "Infolist {} with arguments '{}':", 85 | infolist_name, 86 | infolist_args.unwrap_or_default() 87 | ), 88 | ); 89 | 90 | let mut infolist = infolist.peekable(); 91 | 92 | if infolist.peek().is_none() { 93 | buffer.print(""); 94 | buffer.print_date_tags(0, &["no_filter"], "Empty infolist."); 95 | } else { 96 | for (count, item) in infolist.enumerate() { 97 | buffer.print(""); 98 | 99 | let mut prefix = format!( 100 | "{}[{}{}{}]{}\t", 101 | Weechat::color("chat_delimiters"), 102 | Weechat::color("chat_buffer"), 103 | count, 104 | Weechat::color("chat_delimiters"), 105 | Weechat::color("reset"), 106 | ); 107 | 108 | for (name, value) in sorted(&item) { 109 | let (value_type, value) = match value { 110 | InfolistVariable::Buffer(b) => ( 111 | "ptr", 112 | format!( 113 | "{}{:?}{}", 114 | Weechat::color("green"), 115 | b, 116 | Weechat::color("chat") 117 | ), 118 | ), 119 | InfolistVariable::Integer(i) => ( 120 | "int", 121 | format!( 122 | "{}{}{}", 123 | Weechat::color("yellow"), 124 | i.to_string(), 125 | Weechat::color("chat") 126 | ), 127 | ), 128 | InfolistVariable::String(s) => ( 129 | "str", 130 | format!( 131 | "'{}{}{}'", 132 | Weechat::color("cyan"), 133 | s, 134 | Weechat::color("chat") 135 | ), 136 | ), 137 | InfolistVariable::Time(t) => ( 138 | "tim", 139 | format!( 140 | "{}{}{}", 141 | Weechat::color("lightblue"), 142 | DateTime::::from(t).format("%F %T"), 143 | Weechat::color("chat") 144 | ), 145 | ), 146 | }; 147 | 148 | buffer.print_date_tags( 149 | 0, 150 | &["no_filter"], 151 | &format!( 152 | "{}{:.<30} {}{}{} {}", 153 | prefix, 154 | name, 155 | Weechat::color("brown"), 156 | value_type, 157 | Weechat::color("chat"), 158 | value 159 | ), 160 | ); 161 | 162 | prefix = "".to_string(); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | impl BufferInputCallback for InnerInfolist { 170 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer, input: Cow) -> Result<(), ()> { 171 | match input.as_ref() { 172 | "q" | "Q" => buffer.close(), 173 | _ => self.display_infolist(weechat, buffer, &input), 174 | } 175 | 176 | Ok(()) 177 | } 178 | } 179 | 180 | impl BufferCloseCallback for InnerInfolist { 181 | fn callback(&mut self, _: &Weechat, _: &Buffer) -> Result<(), ()> { 182 | self.buffer.borrow_mut().take(); 183 | Ok(()) 184 | } 185 | } 186 | 187 | impl CommandCallback for InnerInfolist { 188 | fn callback(&mut self, weechat: &Weechat, _: &Buffer, mut arguments: Args) { 189 | if self.buffer.borrow().is_none() { 190 | let buffer = BufferBuilder::new("infolist") 191 | .input_callback(self.clone()) 192 | .close_callback(self.clone()) 193 | .build() 194 | .expect("Can't create infolist buffer"); 195 | let b = buffer.upgrade().unwrap(); 196 | 197 | b.set_localvar("no_log", "1"); 198 | b.disable_time_for_each_line(); 199 | self.set_title(weechat, &b); 200 | 201 | *self.buffer.borrow_mut() = Some(buffer); 202 | } 203 | 204 | let buffer_cell = self.buffer.borrow(); 205 | let buffer_handle = buffer_cell.as_ref().expect("Buffer wasn't created"); 206 | let buffer = buffer_handle 207 | .upgrade() 208 | .expect("Buffer was closed but the handle was still around"); 209 | 210 | arguments.next(); 211 | 212 | let args: String = arguments.collect::>().join(" "); 213 | 214 | if !args.is_empty() { 215 | self.display_infolist(weechat, &buffer, &args); 216 | } 217 | 218 | buffer.switch_to(); 219 | } 220 | } 221 | 222 | impl Plugin for Infolist { 223 | fn init(_: &Weechat, _args: Args) -> Result { 224 | let command_settings = CommandSettings::new("infolist") 225 | .description("Display an infolist and it's items in a buffer") 226 | .add_argument("[infolist_name]") 227 | .add_argument("[arguments]") 228 | .arguments_description( 229 | " infolist: name of infolist\n\ 230 | arguments: optional arguments for infolist\n\n\ 231 | The command without any arguments will just open the infolist \ 232 | buffer.\n\n\ 233 | 234 | Inside the infolist buffer a name of an infolist can be entered \ 235 | with optional arguments.\n\ 236 | 237 | Enter 'q' to close the infolist buffer.\n\n\ 238 | Examples:\n \ 239 | Show information about the nick \"FlashCode\" in the channel \ 240 | \"#weechat\" on the server \"freenode\":\n \ 241 | /infolist irc_nick freenode,#weechat,FlashCode", 242 | ) 243 | .add_completion("%(infolists)"); 244 | let command = Command::new(command_settings, InnerInfolist::default())?; 245 | 246 | Ok(Infolist { command }) 247 | } 248 | } 249 | 250 | plugin!( 251 | Infolist, 252 | name: "infolist", 253 | author: "Damir Jelić ", 254 | description: "Display a infolist and it's items in a buffer", 255 | version: "0.1.0", 256 | license: "GPL3" 257 | ); 258 | -------------------------------------------------------------------------------- /crates/weechat/examples/sample/.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /crates/weechat/examples/sample/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weechat-sample" 3 | version = "0.1.0" 4 | authors = ["Damir Jelić "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "rust" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | weechat = { path = "../.." } 13 | -------------------------------------------------------------------------------- /crates/weechat/examples/sample/Makefile: -------------------------------------------------------------------------------- 1 | WEECHAT_HOME ?= $(HOME)/.weechat 2 | PREFIX ?= $(WEECHAT_HOME) 3 | 4 | .PHONY: install install-dir lint 5 | 6 | target/debug/libgo.so: src/lib.rs 7 | cargo build 8 | 9 | install: install-dir target/debug/librust.so 10 | install -m644 target/debug/librust.so $(DESTDIR)$(PREFIX)/plugins/rust.so 11 | 12 | install-dir: 13 | install -d $(DESTDIR)$(PREFIX)/plugins 14 | 15 | lint: 16 | cargo clippy 17 | -------------------------------------------------------------------------------- /crates/weechat/examples/sample/README.md: -------------------------------------------------------------------------------- 1 | # sample 2 | 3 | Weechat sample plugin. 4 | 5 | This is a sample plugin, showcasing how to create a simple Rust plugin and how 6 | to do some common things inside a Weechat plugin. 7 | 8 | ## Build 9 | 10 | To build the plugin 11 | ``` 12 | make 13 | ``` 14 | 15 | Installation can be done like so 16 | 17 | ``` 18 | make install 19 | ``` 20 | 21 | By default this will install the plugin in your `$HOME/.weechat/plugins` directory. 22 | 23 | ## Picking the correct Weechat version. 24 | 25 | By default the system-wide `weechat-plugin.h` file will be used if found, 26 | this behaviour can be overridden with two environment flags. 27 | 28 | To prefer a bundled include file `WEECHAT_BUNDLED` should be set to `true`. The 29 | bundled include file tracks the latest Weechat release. 30 | 31 | A custom include file can be set with the `WEECHAT_PLUGIN_FILE` environment 32 | variable, this environment variable takes a full path to the include file. 33 | 34 | After an adequate `weechat-plugin.h` file is found rebuild the plugin like so 35 | 36 | ``` 37 | WEECHAT_PLUGIN_FILE=/home/example/weechat-plugin.h make install 38 | ``` 39 | -------------------------------------------------------------------------------- /crates/weechat/examples/sample/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, time::Instant}; 2 | use weechat::{ 3 | buffer::{Buffer, BufferBuilder, NickSettings}, 4 | config::{ 5 | BooleanOption, BooleanOptionSettings, Conf, Config, 6 | ConfigSectionSettings, 7 | }, 8 | hooks::{BarItem, Command, CommandSettings, SignalData, SignalHook}, 9 | plugin, Args, Plugin, ReturnCode, Weechat, 10 | }; 11 | 12 | struct SamplePlugin { 13 | _rust_hook: Command, 14 | _rust_config: Config, 15 | _item: BarItem, 16 | _signal: SignalHook, 17 | } 18 | 19 | impl SamplePlugin { 20 | fn input_cb( 21 | _weechat: &Weechat, 22 | buffer: &Buffer, 23 | input: Cow, 24 | ) -> Result<(), ()> { 25 | buffer.print(&input); 26 | Ok(()) 27 | } 28 | 29 | fn close_cb(_weechat: &Weechat, _buffer: &Buffer) -> Result<(), ()> { 30 | Weechat::print("Closing buffer"); 31 | Ok(()) 32 | } 33 | 34 | fn rust_command_cb(_weechat: &Weechat, buffer: &Buffer, args: Args) { 35 | buffer.print("Hello world"); 36 | 37 | for arg in args { 38 | buffer.print(&arg) 39 | } 40 | } 41 | 42 | fn option_change_cb(_weechat: &Weechat, _option: &BooleanOption) { 43 | Weechat::print("Changing rust option"); 44 | } 45 | } 46 | 47 | impl Plugin for SamplePlugin { 48 | fn init(_: &Weechat, _args: Args) -> Result { 49 | Weechat::print("Hello Rust!"); 50 | 51 | let buffer_handle = BufferBuilder::new("Test buffer") 52 | .input_callback(SamplePlugin::input_cb) 53 | .close_callback(SamplePlugin::close_cb) 54 | .build() 55 | .expect("Can't create buffer"); 56 | 57 | let buffer = buffer_handle.upgrade().expect("Buffer already closed?"); 58 | 59 | buffer.print("Hello test buffer"); 60 | 61 | let n = 100; 62 | 63 | let now = Instant::now(); 64 | 65 | let op_group = buffer 66 | .add_nicklist_group("operators", "blue", true, None) 67 | .expect("Can't create nick group"); 68 | let emma = op_group 69 | .add_nick( 70 | NickSettings::new("Emma") 71 | .set_color("magenta") 72 | .set_prefix("&") 73 | .set_prefix_color("green"), 74 | ) 75 | .expect("Can't add nick to group"); 76 | 77 | Weechat::print(&format!("Nick name getting test: {}", emma.name())); 78 | 79 | for nick_number in 0..n { 80 | let name = &format!("nick_{}", nick_number); 81 | let nick = NickSettings::new(name); 82 | let _ = buffer.add_nick(nick); 83 | } 84 | 85 | buffer.print(&format!( 86 | "Elapsed time for {} nick additions: {}.{}s.", 87 | n, 88 | now.elapsed().as_secs(), 89 | now.elapsed().subsec_millis() 90 | )); 91 | 92 | let sample_command = CommandSettings::new("rustcommand"); 93 | 94 | let command = 95 | Command::new(sample_command, SamplePlugin::rust_command_cb); 96 | 97 | let mut config = Config::new_with_callback( 98 | "rust_sample", 99 | |_weechat: &Weechat, _config: &Conf| { 100 | Weechat::print("Reloaded config"); 101 | }, 102 | ) 103 | .expect("Can't create new config"); 104 | 105 | { 106 | let section_info = ConfigSectionSettings::new("sample_section"); 107 | 108 | let mut section = config 109 | .new_section(section_info) 110 | .expect("Can't create section"); 111 | 112 | let option_settings = BooleanOptionSettings::new("test_option") 113 | .default_value(false) 114 | .set_change_callback(SamplePlugin::option_change_cb); 115 | 116 | section 117 | .new_boolean_option(option_settings) 118 | .expect("Can't create option"); 119 | } 120 | let item = BarItem::new( 121 | "buffer_plugin", 122 | |_weechat: &Weechat, _buffer: &Buffer| "rust/sample".to_owned(), 123 | ); 124 | 125 | let signal_hook = SignalHook::new( 126 | "buffer_switch", 127 | |_weechat: &Weechat, 128 | _signal_name: &str, 129 | data: Option| { 130 | if let Some(data) = data { 131 | if let SignalData::Buffer(buffer) = data { 132 | buffer.print("Switched buffer") 133 | } 134 | } 135 | 136 | ReturnCode::Ok 137 | }, 138 | ); 139 | 140 | Ok(SamplePlugin { 141 | _rust_hook: command.unwrap(), 142 | _rust_config: config, 143 | _item: item.unwrap(), 144 | _signal: signal_hook.unwrap(), 145 | }) 146 | } 147 | } 148 | 149 | impl Drop for SamplePlugin { 150 | fn drop(&mut self) { 151 | Weechat::print("Bye rust"); 152 | } 153 | } 154 | 155 | plugin!( 156 | SamplePlugin, 157 | name: "rust_sample", 158 | author: "Damir Jelić ", 159 | description: "", 160 | version: "0.1.0", 161 | license: "MIT" 162 | ); 163 | -------------------------------------------------------------------------------- /crates/weechat/src/buffer/lines.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap, ffi::c_void, marker::PhantomData}; 2 | 3 | use weechat_sys::{t_hdata, t_weechat_plugin}; 4 | 5 | use crate::{buffer::Buffer, Weechat}; 6 | 7 | /// An iterator that steps over the lines of the buffer. 8 | pub struct BufferLines<'a> { 9 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 10 | pub(crate) first_line: *mut c_void, 11 | pub(crate) last_line: *mut c_void, 12 | pub(crate) buffer: PhantomData<&'a Buffer<'a>>, 13 | pub(crate) done: bool, 14 | } 15 | 16 | impl<'a> Iterator for BufferLines<'a> { 17 | type Item = BufferLine<'a>; 18 | 19 | fn next(&mut self) -> Option { 20 | if self.done { 21 | None 22 | } else { 23 | let weechat = Weechat::from_ptr(self.weechat_ptr); 24 | 25 | let line_hdata = unsafe { weechat.hdata_get("line") }; 26 | 27 | let line_data_pointer = 28 | unsafe { weechat.hdata_pointer(line_hdata, self.first_line, "data") }; 29 | 30 | if line_data_pointer.is_null() { 31 | return None; 32 | } 33 | 34 | if self.first_line == self.last_line { 35 | self.done = true; 36 | } 37 | 38 | self.first_line = unsafe { weechat.hdata_move(line_hdata, self.first_line, 1) }; 39 | 40 | Some(BufferLine { weechat, line_data_pointer, buffer: PhantomData }) 41 | } 42 | } 43 | } 44 | 45 | impl DoubleEndedIterator for BufferLines<'_> { 46 | fn next_back(&mut self) -> Option { 47 | if self.done { 48 | None 49 | } else { 50 | let weechat = Weechat::from_ptr(self.weechat_ptr); 51 | 52 | let line_hdata = unsafe { weechat.hdata_get("line") }; 53 | 54 | let line_data_pointer = 55 | unsafe { weechat.hdata_pointer(line_hdata, self.last_line, "data") }; 56 | 57 | if line_data_pointer.is_null() { 58 | return None; 59 | } 60 | 61 | if self.last_line == self.first_line { 62 | self.done = true; 63 | } 64 | 65 | self.last_line = unsafe { weechat.hdata_move(line_hdata, self.last_line, -1) }; 66 | 67 | Some(BufferLine { weechat, line_data_pointer, buffer: PhantomData }) 68 | } 69 | } 70 | } 71 | 72 | /// Struct that can be used to update multiple line fields at once. 73 | #[allow(missing_docs)] 74 | #[derive(Debug, Clone, Default)] 75 | pub struct LineData<'a> { 76 | pub prefix: Option<&'a str>, 77 | pub message: Option<&'a str>, 78 | pub date: Option, 79 | pub date_printed: Option, 80 | pub tags: Option<&'a [&'a str]>, 81 | } 82 | 83 | /// The buffer line, makes it possible to modify the printed message and other 84 | /// line data. 85 | pub struct BufferLine<'a> { 86 | weechat: Weechat, 87 | line_data_pointer: *mut c_void, 88 | buffer: PhantomData<&'a Buffer<'a>>, 89 | } 90 | 91 | impl<'a> BufferLine<'a> { 92 | fn hdata(&self) -> *mut t_hdata { 93 | unsafe { self.weechat.hdata_get("line_data") } 94 | } 95 | 96 | fn update_line(&self, hashmap: HashMap<&str, &str>) { 97 | unsafe { 98 | self.weechat.hdata_update(self.hdata(), self.line_data_pointer, hashmap); 99 | } 100 | } 101 | 102 | /// Get the prefix of the line, everything left of the message separator 103 | /// (usually `|`) is considered the prefix. 104 | pub fn prefix(&self) -> Cow { 105 | unsafe { self.weechat.hdata_string(self.hdata(), self.line_data_pointer, "prefix") } 106 | } 107 | 108 | /// Set the prefix to the given new value. 109 | /// 110 | /// # Arguments 111 | /// 112 | /// * `new_prefix` - The new prefix that should be set on the line. 113 | pub fn set_prefix(&self, new_prefix: &str) { 114 | let mut hashmap = HashMap::new(); 115 | hashmap.insert("prefix", new_prefix); 116 | self.update_line(hashmap); 117 | } 118 | 119 | /// Get the message of the line. 120 | pub fn message(&self) -> Cow { 121 | unsafe { self.weechat.hdata_string(self.hdata(), self.line_data_pointer, "message") } 122 | } 123 | 124 | /// Set the message to the given new value. 125 | /// 126 | /// # Arguments 127 | /// 128 | /// * `new_value` - The new message that should be set on the line. 129 | pub fn set_message(&self, new_value: &str) { 130 | let mut hashmap = HashMap::new(); 131 | 132 | hashmap.insert("message", new_value); 133 | self.update_line(hashmap); 134 | } 135 | 136 | /// Get the date of the line. 137 | pub fn date(&self) -> isize { 138 | unsafe { self.weechat.hdata_time(self.hdata(), self.line_data_pointer, "date") } 139 | } 140 | 141 | /// Set the date to the given new value. 142 | /// 143 | /// # Arguments 144 | /// 145 | /// * `new_value` - The new date that should be set on the line. 146 | pub fn set_date(&self, new_value: i64) { 147 | let mut hashmap = HashMap::new(); 148 | let date = new_value.to_string(); 149 | hashmap.insert("date", date.as_ref()); 150 | self.update_line(hashmap); 151 | } 152 | 153 | /// Get the date the line was printed. 154 | pub fn date_printed(&self) -> isize { 155 | unsafe { self.weechat.hdata_time(self.hdata(), self.line_data_pointer, "date_printed") } 156 | } 157 | 158 | /// Set the date the line was printed to the given new value. 159 | /// 160 | /// # Arguments 161 | /// 162 | /// * `new_value` - The new date that should be set on the line. 163 | pub fn set_date_printed(&self, new_value: &str) { 164 | let mut hashmap = HashMap::new(); 165 | let date = new_value.to_string(); 166 | hashmap.insert("date_printed", date.as_ref()); 167 | self.update_line(hashmap); 168 | } 169 | 170 | /// Is the line highlighted. 171 | pub fn highlighted(&self) -> bool { 172 | unsafe { self.weechat.hdata_char(self.hdata(), self.line_data_pointer, "highlight") != 0 } 173 | } 174 | 175 | /// Get the list of tags of the line. 176 | pub fn tags(&self) -> Vec> { 177 | unsafe { 178 | let count = self.weechat.hdata_var_array_size( 179 | self.hdata(), 180 | self.line_data_pointer, 181 | "tags_array", 182 | ); 183 | 184 | let mut tags = Vec::with_capacity(count as usize); 185 | 186 | for i in 0..count { 187 | let tag = self.weechat.hdata_string( 188 | self.hdata(), 189 | self.line_data_pointer, 190 | &format!("{i}|tags_array"), 191 | ); 192 | tags.push(tag); 193 | } 194 | 195 | tags 196 | } 197 | } 198 | 199 | /// Set the tags of the line to the new value. 200 | /// 201 | /// # Arguments 202 | /// 203 | /// * `new_value` - The new tags that should be set on the line. 204 | pub fn set_tags(&self, new_value: &[&str]) { 205 | let mut hashmap = HashMap::new(); 206 | let tags = new_value.join(","); 207 | hashmap.insert("tags_array", tags.as_ref()); 208 | self.update_line(hashmap); 209 | } 210 | 211 | /// Update multiple fields of the line at once. 212 | /// 213 | /// # Arguments 214 | /// * `data` - `LineData` that contains new values that should be set on the 215 | /// line. 216 | /// 217 | /// # Example 218 | /// ```no_run 219 | /// # use weechat::Weechat; 220 | /// # use weechat::buffer::{BufferBuilder, LineData}; 221 | /// # let buffer_handle = BufferBuilder::new("test") 222 | /// # .build() 223 | /// # .unwrap(); 224 | /// # let buffer = buffer_handle.upgrade().unwrap(); 225 | /// # let mut lines = buffer.lines(); 226 | /// # let line = lines.next().unwrap(); 227 | /// 228 | /// let new_line = LineData { 229 | /// message: Some("Hello world"), 230 | /// tags: Some(&["First", "Second tag"]), 231 | /// .. Default::default() 232 | /// }; 233 | /// 234 | /// line.update(new_line) 235 | /// ``` 236 | pub fn update(&self, data: LineData<'a>) { 237 | let mut hashmap = HashMap::new(); 238 | 239 | let tags = data.tags.map(|t| t.join(",")); 240 | let date = data.date.map(|d| d.to_string()); 241 | let date_printed = data.date_printed.map(|d| d.to_string()); 242 | 243 | if let Some(message) = data.message { 244 | hashmap.insert("message", message); 245 | } 246 | 247 | if let Some(prefix) = data.prefix { 248 | hashmap.insert("prefix", prefix); 249 | } 250 | 251 | if let Some(t) = tags.as_ref() { 252 | hashmap.insert("tags_array", t); 253 | } 254 | 255 | if let Some(d) = date.as_ref() { 256 | hashmap.insert("date", d); 257 | } 258 | 259 | if let Some(d) = date_printed.as_ref() { 260 | hashmap.insert("date_printed", d); 261 | } 262 | 263 | self.update_line(hashmap); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /crates/weechat/src/buffer/nick.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, marker::PhantomData}; 2 | 3 | use weechat_sys::{t_gui_buffer, t_gui_nick, t_weechat_plugin}; 4 | 5 | use crate::{buffer::Buffer, LossyCString, Weechat}; 6 | 7 | /// Settings to create a new nick. 8 | pub struct NickSettings<'a> { 9 | /// Name of the new nick. 10 | pub(crate) name: &'a str, 11 | /// Color for the nick. 12 | pub(crate) color: &'a str, 13 | /// Prefix that will be shown before the name. 14 | pub(crate) prefix: &'a str, 15 | /// Color of the prefix. 16 | pub(crate) prefix_color: &'a str, 17 | /// Should the nick be visible in the nicklist. 18 | pub(crate) visible: bool, 19 | } 20 | 21 | impl<'a> NickSettings<'a> { 22 | /// Create new empyt nick creation settings. 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `name` - The name of the new nick. 27 | pub fn new(name: &str) -> NickSettings { 28 | NickSettings { name, color: "", prefix: "", prefix_color: "", visible: true } 29 | } 30 | 31 | /// Set the color of the nick. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `color` - The color that the nick should have. 36 | pub fn set_color(mut self, color: &'a str) -> NickSettings<'a> { 37 | self.color = color; 38 | self 39 | } 40 | 41 | /// Set the prefix of the nick. 42 | /// 43 | /// # Arguments 44 | /// 45 | /// * `prefix` - The prefix displayed before the nick in the nicklist. 46 | pub fn set_prefix(mut self, prefix: &'a str) -> NickSettings<'a> { 47 | self.prefix = prefix; 48 | self 49 | } 50 | 51 | /// Set the color of the nick prefix. 52 | /// 53 | /// # Arguments 54 | /// 55 | /// * `prefix_color` - The color that the prefix should have. 56 | pub fn set_prefix_color(mut self, prefix_color: &'a str) -> NickSettings<'a> { 57 | self.prefix_color = prefix_color; 58 | self 59 | } 60 | 61 | /// Set the visibility of the nick. 62 | /// 63 | /// # Arguments 64 | /// 65 | /// * `visible` - Should the nick be visible in the nicklist, `true` if it 66 | /// should be visible, false otherwise. Defaults to `true`. 67 | pub fn set_visible(mut self, visible: bool) -> NickSettings<'a> { 68 | self.visible = visible; 69 | self 70 | } 71 | } 72 | 73 | /// Weechat Nick type 74 | pub struct Nick<'a> { 75 | pub(crate) ptr: *mut t_gui_nick, 76 | pub(crate) buf_ptr: *mut t_gui_buffer, 77 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 78 | pub(crate) buffer: PhantomData<&'a Buffer<'a>>, 79 | } 80 | 81 | impl Nick<'_> { 82 | /// Get a Weechat object out of the nick. 83 | fn get_weechat(&self) -> Weechat { 84 | Weechat::from_ptr(self.weechat_ptr) 85 | } 86 | 87 | /// Get a string property of the nick. 88 | /// * `property` - The name of the property to get the value for, this can 89 | /// be one of name, color, prefix or prefix_color. If a unknown property 90 | /// is requested an empty string is returned. 91 | fn get_string(&self, property: &str) -> Option> { 92 | let weechat = self.get_weechat(); 93 | let get_string = weechat.get().nicklist_nick_get_string.unwrap(); 94 | let c_property = LossyCString::new(property); 95 | unsafe { 96 | let ret = get_string(self.buf_ptr, self.ptr, c_property.as_ptr()); 97 | 98 | if ret.is_null() { 99 | None 100 | } else { 101 | Some(CStr::from_ptr(ret).to_string_lossy()) 102 | } 103 | } 104 | } 105 | 106 | /// Get the name property of the nick. 107 | pub fn name(&self) -> Cow { 108 | self.get_string("name").unwrap() 109 | } 110 | 111 | /// Get the color of the nick. 112 | pub fn color(&self) -> Cow { 113 | self.get_string("color").unwrap() 114 | } 115 | 116 | /// Get the prefix of the nick. 117 | pub fn prefix(&self) -> Cow { 118 | self.get_string("prefix").unwrap() 119 | } 120 | 121 | /// Get the color of the nick prefix. 122 | pub fn prefix_color(&self) -> Cow { 123 | self.get_string("prefix_color").unwrap() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/weechat/src/buffer/nickgroup.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, marker::PhantomData}; 2 | 3 | use weechat_sys::{t_gui_buffer, t_gui_nick_group, t_weechat_plugin}; 4 | 5 | use crate::{ 6 | buffer::{Buffer, Nick, NickSettings}, 7 | LossyCString, Weechat, 8 | }; 9 | 10 | /// Weechat nicklist Group type. 11 | pub struct NickGroup<'a> { 12 | pub(crate) ptr: *mut t_gui_nick_group, 13 | pub(crate) buf_ptr: *mut t_gui_buffer, 14 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 15 | pub(crate) buffer: PhantomData<&'a Buffer<'a>>, 16 | } 17 | 18 | impl NickGroup<'_> { 19 | fn get_weechat(&self) -> Weechat { 20 | Weechat::from_ptr(self.weechat_ptr) 21 | } 22 | 23 | /// Get a string property of the nick. 24 | /// * `property` - The name of the property to get the value for, this can 25 | /// be one of name, color, prefix or prefix_color. If a unknown property 26 | /// is requested an empty string is returned. 27 | fn get_string(&self, property: &str) -> Option> { 28 | let weechat = self.get_weechat(); 29 | let get_string = weechat.get().nicklist_group_get_string.unwrap(); 30 | let c_property = LossyCString::new(property); 31 | 32 | let ret = unsafe { get_string(self.buf_ptr, self.ptr, c_property.as_ptr()) }; 33 | 34 | if ret.is_null() { 35 | None 36 | } else { 37 | unsafe { Some(CStr::from_ptr(ret).to_string_lossy()) } 38 | } 39 | } 40 | 41 | fn get_integer(&self, property: &str) -> i32 { 42 | let weechat = self.get_weechat(); 43 | let get_integer = weechat.get().nicklist_group_get_integer.unwrap(); 44 | let c_property = LossyCString::new(property); 45 | 46 | unsafe { get_integer(self.buf_ptr, self.ptr, c_property.as_ptr()) } 47 | } 48 | 49 | /// Get the name of the group. 50 | pub fn name(&self) -> Cow { 51 | self.get_string("name").unwrap() 52 | } 53 | 54 | /// Get the color of the group. 55 | pub fn color(&self) -> Cow { 56 | self.get_string("color").unwrap() 57 | } 58 | 59 | /// Is the nick group visible. 60 | pub fn visible(&self) -> bool { 61 | self.get_integer("visible") != 0 62 | } 63 | 64 | /// Get the group nesting level. 65 | /// 66 | /// Returns 0 if this is the root group, 1 if it's a child of the root 67 | /// group. 68 | pub fn level(&self) -> u32 { 69 | self.get_integer("level") as u32 70 | } 71 | 72 | /// Create and add a new nick to the buffer nicklist under this group. 73 | /// 74 | /// # Arguments 75 | /// 76 | /// * `nick_settings` - Nick arguments struct for the nick that should be 77 | /// added. 78 | /// 79 | /// Returns the newly created nick if one is created successfully, an empty 80 | /// error otherwise. 81 | pub fn add_nick(&self, nick_settings: NickSettings) -> Result { 82 | let weechat = self.get_weechat(); 83 | let nick_ptr = Buffer::add_nick_helper(&weechat, self.buf_ptr, nick_settings, Some(self)); 84 | 85 | if nick_ptr.is_null() { 86 | return Err(()); 87 | } 88 | 89 | Ok(Nick { 90 | ptr: nick_ptr, 91 | buf_ptr: self.buf_ptr, 92 | weechat_ptr: self.get_weechat().ptr, 93 | buffer: PhantomData, 94 | }) 95 | } 96 | 97 | /// Search for a nick in this nick group. 98 | /// 99 | /// # Arguments 100 | /// 101 | /// * `nick` - The name of the nick that should be found. 102 | /// 103 | /// Returns a `Nick` if one is found, None otherwise. 104 | pub fn search_nick(&self, nick: &str) -> Option { 105 | let weechat = self.get_weechat(); 106 | let nick = Buffer::search_nick_helper(&weechat, self.buf_ptr, nick, None); 107 | 108 | if nick.is_null() { 109 | None 110 | } else { 111 | Some(Nick { 112 | ptr: nick, 113 | buf_ptr: self.buf_ptr, 114 | weechat_ptr: weechat.ptr, 115 | buffer: PhantomData, 116 | }) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/weechat/src/buffer/window.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, ptr}; 2 | 3 | use weechat_sys::{t_gui_window, t_weechat_plugin}; 4 | 5 | use super::Buffer; 6 | use crate::{LossyCString, Weechat}; 7 | 8 | /// A Weechat window. 9 | /// 10 | /// A window is a screen area which displays a buffer. It is possible to split 11 | /// your screen into many windows. 12 | pub struct Window<'a> { 13 | pub(crate) weechat: *mut t_weechat_plugin, 14 | pub(crate) ptr: *mut t_gui_window, 15 | pub(crate) phantom: PhantomData<&'a Buffer<'a>>, 16 | } 17 | 18 | impl Window<'_> { 19 | fn get_integer(&self, property: &str) -> i32 { 20 | let weechat = Weechat::from_ptr(self.weechat); 21 | let get_integer = weechat.get().window_get_integer.unwrap(); 22 | let property = LossyCString::new(property); 23 | 24 | unsafe { get_integer(self.ptr, property.as_ptr()) } 25 | } 26 | 27 | fn get_bool(&self, property: &str) -> bool { 28 | self.get_integer(property) == 1 29 | } 30 | 31 | /// The number of the window. 32 | pub fn number(&self) -> i32 { 33 | self.get_integer("number") 34 | } 35 | 36 | /// The X coordinate position of the window in the terminal (the first 37 | /// column is 0). 38 | pub fn x(&self) -> i32 { 39 | self.get_integer("win_x") 40 | } 41 | 42 | /// The Y coordinate position of the window in the terminal (the first 43 | /// line is 0). 44 | pub fn y(&self) -> i32 { 45 | self.get_integer("win_y") 46 | } 47 | 48 | /// The width of the window in chars. 49 | pub fn width(&self) -> i32 { 50 | self.get_integer("win_width") 51 | } 52 | 53 | /// The height of the window in chars. 54 | pub fn height(&self) -> i32 { 55 | self.get_integer("win_height") 56 | } 57 | 58 | /// The width of the window expressed as a percentage of the parent window, 59 | /// for example 50 means that the window is half of the size of the parent 60 | /// window. 61 | pub fn width_percentage(&self) -> i32 { 62 | self.get_integer("win_width_pct") 63 | } 64 | 65 | /// The height of the window expressed as a percentage of the parent window, 66 | /// for example 50 means that the window is half of the size of the parent 67 | /// window. 68 | pub fn height_percentage(&self) -> i32 { 69 | self.get_integer("win_height_pct") 70 | } 71 | 72 | /// The X coordinate position of the chat window in the terminal (the first 73 | /// column is 0). 74 | pub fn chat_x(&self) -> i32 { 75 | self.get_integer("win_chat_x") 76 | } 77 | 78 | /// The Y coordinate position of the chat window in the terminal (the first 79 | /// line is 0). 80 | pub fn chat_y(&self) -> i32 { 81 | self.get_integer("win_chat_y") 82 | } 83 | 84 | /// The width of the chat window in chars. 85 | pub fn chat_width(&self) -> i32 { 86 | self.get_integer("win_chat_width") 87 | } 88 | 89 | /// The height of the chat window in chars. 90 | pub fn chat_height(&self) -> i32 { 91 | self.get_integer("win_chat_height") 92 | } 93 | 94 | /// Returns true if the first line of the buffer is shown in the window, or 95 | /// to put it differently if the window is scrolled completely up. 96 | pub fn is_first_line_displayed(&self) -> bool { 97 | self.get_bool("first_line_displayed") 98 | } 99 | 100 | /// Returns true if the last line of the buffer is shown in the window, or 101 | /// to put it differently if the window is scrolled completely down. 102 | pub fn is_last_line_displayed(&self) -> bool { 103 | self.get_bool("scrolling") 104 | } 105 | 106 | /// This gives the number of lines that are not displayed towards the bottom 107 | /// of the buffer. 108 | pub fn lines_after(&self) -> i32 { 109 | self.get_integer("lines_after") 110 | } 111 | 112 | fn set_title_helper(&self, title: Option<&str>) { 113 | let weechat = Weechat::from_ptr(self.weechat); 114 | let set_title = weechat.get().window_set_title.unwrap(); 115 | 116 | if let Some(title) = title { 117 | let title = LossyCString::new(title); 118 | unsafe { 119 | set_title(title.as_ptr()); 120 | } 121 | } else { 122 | unsafe { 123 | set_title(ptr::null_mut()); 124 | } 125 | }; 126 | } 127 | 128 | /// Set the title for the terminal. 129 | /// 130 | /// # Arguments 131 | /// 132 | /// * `title` - The new title that should be set for the terminal, the 133 | /// string is evaluated, so variables like ${info:version} can be used. 134 | pub fn set_title(&self, title: &str) { 135 | self.set_title_helper(Some(title)); 136 | } 137 | 138 | /// Reset the title for the terminal. 139 | pub fn reset_title(&self) { 140 | self.set_title_helper(None); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /crates/weechat/src/config/boolean.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use weechat_sys::{t_config_option, t_weechat_plugin}; 4 | 5 | use crate::{ 6 | config::{ 7 | config_options::{FromPtrs, HiddenConfigOptionT}, 8 | BaseConfigOption, ConfigOptions, ConfigSection, 9 | }, 10 | Weechat, 11 | }; 12 | 13 | type BooleanChangeCallback = Box; 14 | 15 | /// Settings for a new boolean option. 16 | #[derive(Default)] 17 | pub struct BooleanOptionSettings { 18 | pub(crate) name: String, 19 | 20 | pub(crate) description: String, 21 | 22 | pub(crate) default_value: bool, 23 | 24 | pub(crate) change_cb: Option, 25 | } 26 | 27 | impl BooleanOptionSettings { 28 | /// Create new settings that can be used to create a new boolean option. 29 | /// 30 | /// # Arguments 31 | /// 32 | /// * `name` - The name of the new option. 33 | pub fn new>(name: N) -> Self { 34 | BooleanOptionSettings { name: name.into(), ..Default::default() } 35 | } 36 | 37 | /// Set the description of the option. 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `description` - The description of the new option. 42 | pub fn description>(mut self, description: D) -> Self { 43 | self.description = description.into(); 44 | self 45 | } 46 | 47 | /// Set the default value of the option. 48 | /// 49 | /// This is the value the option will have if it isn't set by the user. If 50 | /// the option is reset, the option will take this value. 51 | /// 52 | /// # Arguments 53 | /// 54 | /// * `value` - The value that should act as the default value. 55 | pub fn default_value(mut self, value: bool) -> Self { 56 | self.default_value = value; 57 | self 58 | } 59 | 60 | /// Set the callback that will run when the value of the option changes. 61 | /// 62 | /// # Arguments 63 | /// 64 | /// * `callback` - The callback that will be run. 65 | /// 66 | /// # Examples 67 | /// ``` 68 | /// use weechat::Weechat; 69 | /// use weechat::config::BooleanOptionSettings; 70 | /// 71 | /// let settings = BooleanOptionSettings::new("autoconnect") 72 | /// .set_change_callback(|weechat, option| { 73 | /// Weechat::print("Option changed"); 74 | /// }); 75 | /// ``` 76 | pub fn set_change_callback( 77 | mut self, 78 | callback: impl FnMut(&Weechat, &BooleanOption) + 'static, 79 | ) -> Self { 80 | self.change_cb = Some(Box::new(callback)); 81 | self 82 | } 83 | } 84 | 85 | /// A config option with a boolean value. 86 | pub struct BooleanOption<'a> { 87 | pub(crate) ptr: *mut t_config_option, 88 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 89 | pub(crate) _phantom: PhantomData<&'a ConfigSection>, 90 | } 91 | 92 | impl BooleanOption<'_> { 93 | /// Get the value of the option. 94 | pub fn value(&self) -> bool { 95 | let weechat = self.get_weechat(); 96 | let config_boolean = weechat.get().config_boolean.unwrap(); 97 | let ret = unsafe { config_boolean(self.get_ptr()) }; 98 | ret != 0 99 | } 100 | } 101 | 102 | impl FromPtrs for BooleanOption<'_> { 103 | fn from_ptrs(option_ptr: *mut t_config_option, weechat_ptr: *mut t_weechat_plugin) -> Self { 104 | BooleanOption { ptr: option_ptr, weechat_ptr, _phantom: PhantomData } 105 | } 106 | } 107 | 108 | impl HiddenConfigOptionT for BooleanOption<'_> { 109 | fn get_ptr(&self) -> *mut t_config_option { 110 | self.ptr 111 | } 112 | 113 | fn get_weechat(&self) -> Weechat { 114 | Weechat::from_ptr(self.weechat_ptr) 115 | } 116 | } 117 | 118 | impl BaseConfigOption for BooleanOption<'_> {} 119 | impl ConfigOptions for BooleanOption<'_> {} 120 | 121 | impl PartialEq for BooleanOption<'_> { 122 | fn eq(&self, other: &bool) -> bool { 123 | self.value() == *other 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/weechat/src/config/color.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, marker::PhantomData}; 2 | 3 | use weechat_sys::{t_config_option, t_weechat_plugin}; 4 | 5 | use crate::{ 6 | config::{ 7 | config_options::{ConfigOptions, FromPtrs, HiddenConfigOptionT}, 8 | BaseConfigOption, ConfigSection, 9 | }, 10 | Weechat, 11 | }; 12 | 13 | type ColorChangeCallback = Box; 14 | 15 | /// Settings for a new color option. 16 | #[derive(Default)] 17 | pub struct ColorOptionSettings { 18 | pub(crate) name: String, 19 | 20 | pub(crate) description: String, 21 | 22 | pub(crate) default_value: String, 23 | 24 | pub(crate) change_cb: Option, 25 | } 26 | 27 | impl ColorOptionSettings { 28 | /// Create new settings that can be used to create a new color option. 29 | /// 30 | /// # Arguments 31 | /// 32 | /// * `name` - The name of the new option. 33 | pub fn new>(name: N) -> Self { 34 | ColorOptionSettings { name: name.into(), ..Default::default() } 35 | } 36 | 37 | /// Set the description of the option. 38 | /// 39 | /// # Arguments 40 | /// * `description` - The description of the new option. 41 | pub fn description>(mut self, description: D) -> Self { 42 | self.description = description.into(); 43 | self 44 | } 45 | 46 | /// Set the default value of the option. 47 | /// 48 | /// This is the value the option will have if it isn't set by the user. If 49 | /// the option is reset, the option will take this value. 50 | /// 51 | /// # Arguments 52 | /// 53 | /// * `value` - The value that should act as the default value. 54 | pub fn default_value>(mut self, value: V) -> Self { 55 | self.default_value = value.into(); 56 | self 57 | } 58 | 59 | /// Set the callback that will run when the value of the option changes. 60 | /// 61 | /// # Arguments 62 | /// 63 | /// * `callback` - The callback that will be run. 64 | /// 65 | /// # Examples 66 | /// ``` 67 | /// use weechat::Weechat; 68 | /// use weechat::config::ColorOptionSettings; 69 | /// 70 | /// let settings = ColorOptionSettings::new("address") 71 | /// .set_change_callback(|weechat, option| { 72 | /// Weechat::print("Option changed"); 73 | /// }); 74 | /// ``` 75 | pub fn set_change_callback( 76 | mut self, 77 | callback: impl FnMut(&Weechat, &ColorOption) + 'static, 78 | ) -> Self { 79 | self.change_cb = Some(Box::new(callback)); 80 | self 81 | } 82 | } 83 | 84 | /// A config option with a color value. 85 | pub struct ColorOption<'a> { 86 | pub(crate) ptr: *mut t_config_option, 87 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 88 | pub(crate) _phantom: PhantomData<&'a ConfigSection>, 89 | } 90 | 91 | impl ColorOption<'_> { 92 | /// Get the value of the option. 93 | pub fn value(&self) -> Cow { 94 | let weechat = self.get_weechat(); 95 | let config_string = weechat.get().config_string.unwrap(); 96 | unsafe { 97 | let string = config_string(self.get_ptr()); 98 | CStr::from_ptr(string).to_string_lossy() 99 | } 100 | } 101 | } 102 | 103 | impl FromPtrs for ColorOption<'_> { 104 | fn from_ptrs(option_ptr: *mut t_config_option, weechat_ptr: *mut t_weechat_plugin) -> Self { 105 | ColorOption { ptr: option_ptr, weechat_ptr, _phantom: PhantomData } 106 | } 107 | } 108 | 109 | impl HiddenConfigOptionT for ColorOption<'_> { 110 | fn get_ptr(&self) -> *mut t_config_option { 111 | self.ptr 112 | } 113 | 114 | fn get_weechat(&self) -> Weechat { 115 | Weechat::from_ptr(self.weechat_ptr) 116 | } 117 | } 118 | 119 | impl BaseConfigOption for ColorOption<'_> {} 120 | impl ConfigOptions for ColorOption<'_> {} 121 | -------------------------------------------------------------------------------- /crates/weechat/src/config/config_options.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, convert::TryFrom, ffi::CStr}; 2 | 3 | use weechat_sys::{t_config_option, t_weechat_plugin}; 4 | 5 | use crate::{config::OptionChanged, LossyCString, Weechat}; 6 | 7 | #[derive(Debug, PartialEq, Clone, Default)] 8 | #[allow(missing_docs)] 9 | pub enum OptionType { 10 | Boolean, 11 | Integer, 12 | #[default] 13 | String, 14 | Color, 15 | Enum, 16 | } 17 | 18 | impl TryFrom<&str> for OptionType { 19 | type Error = &'static str; 20 | fn try_from(value: &str) -> Result { 21 | let ret = match value { 22 | "boolean" => OptionType::Boolean, 23 | "integer" => OptionType::Integer, 24 | "string" => OptionType::String, 25 | "color" => OptionType::Color, 26 | "enum" => OptionType::Enum, 27 | _ => return Err("Invalid option type"), 28 | }; 29 | 30 | Ok(ret) 31 | } 32 | } 33 | 34 | impl OptionType { 35 | pub(crate) fn as_str(&self) -> &'static str { 36 | match self { 37 | OptionType::Boolean => "boolean", 38 | OptionType::Integer => "integer", 39 | OptionType::String => "string", 40 | OptionType::Color => "color", 41 | OptionType::Enum => "enum", 42 | } 43 | } 44 | } 45 | 46 | pub trait FromPtrs { 47 | /// Returns the raw pointer to the config option. 48 | fn from_ptrs(option_ptr: *mut t_config_option, weechat_ptr: *mut t_weechat_plugin) -> Self; 49 | } 50 | 51 | pub trait HiddenConfigOptionT { 52 | /// Returns the raw pointer to the config option. 53 | fn get_ptr(&self) -> *mut t_config_option; 54 | fn get_weechat(&self) -> Weechat; 55 | 56 | fn get_string(&self, property: &str) -> Option> { 57 | let weechat = self.get_weechat(); 58 | let get_string = weechat.get().config_option_get_string.unwrap(); 59 | let property = LossyCString::new(property); 60 | 61 | unsafe { 62 | let string = get_string(self.get_ptr(), property.as_ptr()); 63 | if string.is_null() { 64 | None 65 | } else { 66 | Some(CStr::from_ptr(string).to_string_lossy()) 67 | } 68 | } 69 | } 70 | } 71 | 72 | /// Base configuration option methods. 73 | /// 74 | /// These methods are implemented for every option and don't depend on the 75 | /// option type. 76 | pub trait BaseConfigOption: HiddenConfigOptionT { 77 | /// Get the name of the option. 78 | fn name(&self) -> Cow { 79 | self.get_string("name").expect("Can't get the name of the option") 80 | } 81 | 82 | /// Get the description of the option. 83 | fn description(&self) -> Cow { 84 | self.get_string("description").expect("Can't get the description of the option") 85 | } 86 | 87 | /// Get the section name of the section the option belongs to. 88 | fn section_name(&self) -> Cow { 89 | self.get_string("section_name").expect("Can't get the section name of the option") 90 | } 91 | 92 | /// Get the config name the option belongs to. 93 | fn config_name(&self) -> Cow { 94 | self.get_string("config_name").expect("Can't get the config name of the option") 95 | } 96 | 97 | /// Get the type of the config option 98 | fn option_type(&self) -> OptionType { 99 | let option_type = self.get_string("type").expect("Can't get the config name of the option"); 100 | OptionType::try_from(option_type.as_ref()).unwrap() 101 | } 102 | 103 | /// Resets the option to its default value. 104 | fn reset(&self, run_callback: bool) -> OptionChanged { 105 | let weechat = self.get_weechat(); 106 | let option_reset = weechat.get().config_option_reset.unwrap(); 107 | 108 | let ret = unsafe { option_reset(self.get_ptr(), run_callback as i32) }; 109 | 110 | OptionChanged::from_int(ret) 111 | } 112 | 113 | /// Set the option using a string. 114 | /// 115 | /// Weechat will parse the string and turn it into a appropriate value 116 | /// depending on the option type. 117 | /// 118 | /// # Arguments 119 | /// `value` - The value to which the option should be set. 120 | fn set(&self, value: &str, run_callback: bool) -> OptionChanged { 121 | let value = LossyCString::new(value); 122 | 123 | let weechat = self.get_weechat(); 124 | let option_set = weechat.get().config_option_set.unwrap(); 125 | 126 | let ret = unsafe { option_set(self.get_ptr(), value.as_ptr(), run_callback as i32) }; 127 | 128 | OptionChanged::from_int(ret) 129 | } 130 | 131 | /// Is the option undefined/null. 132 | fn is_null(&self) -> bool { 133 | let weechat = self.get_weechat(); 134 | let is_null = weechat.get().config_option_is_null.unwrap(); 135 | 136 | let ret = unsafe { is_null(self.get_ptr()) }; 137 | 138 | ret != 0 139 | } 140 | } 141 | 142 | /// Marker trait for config options. 143 | pub trait ConfigOptions: BaseConfigOption + FromPtrs {} 144 | 145 | pub(crate) type CheckCB = dyn FnMut(&Weechat, &T, Cow) -> bool; 146 | pub(crate) type OptionCallback = Box; 147 | 148 | pub(crate) struct OptionPointers { 149 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 150 | pub(crate) check_cb: Option>>, 151 | pub(crate) change_cb: Option>, 152 | pub(crate) delete_cb: Option>, 153 | } 154 | -------------------------------------------------------------------------------- /crates/weechat/src/config/enum.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use weechat_sys::{t_config_option, t_weechat_plugin}; 4 | 5 | use crate::{ 6 | config::{ 7 | config_options::{ConfigOptions, FromPtrs, HiddenConfigOptionT}, 8 | BaseConfigOption, ConfigSection, 9 | }, 10 | Weechat, 11 | }; 12 | 13 | type EnumChangeCallback = Box; 14 | 15 | /// Settings for a new enum option. 16 | #[derive(Default)] 17 | pub struct EnumOptionSettings { 18 | pub(crate) name: String, 19 | 20 | pub(crate) description: String, 21 | 22 | pub(crate) default_value: i32, 23 | 24 | pub(crate) min: i32, 25 | 26 | pub(crate) max: i32, 27 | 28 | pub(crate) string_values: String, 29 | 30 | pub(crate) change_cb: Option, 31 | } 32 | 33 | impl EnumOptionSettings { 34 | /// Create new settings that can be used to create a new enum option. 35 | /// 36 | /// An enum option is represented as integer (index of enum value) 37 | /// internally 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `name` - The name of the new option. 42 | pub fn new>(name: N) -> Self { 43 | EnumOptionSettings { name: name.into(), ..Default::default() } 44 | } 45 | 46 | /// Set the description of the option. 47 | /// 48 | /// # Arguments 49 | /// 50 | /// * `description` - The description of the new option. 51 | pub fn description>(mut self, description: D) -> Self { 52 | self.description = description.into(); 53 | self 54 | } 55 | 56 | /// Set the default value of the option. 57 | /// 58 | /// This is the value the option will have if it isn't set by the user. If 59 | /// the option is reset, the option will take this value. 60 | /// 61 | /// # Arguments 62 | /// 63 | /// * `value` - The value that should act as the default value. 64 | pub fn default_value>(mut self, value: V) -> Self { 65 | self.default_value = value.into(); 66 | self 67 | } 68 | 69 | /// Set minimal value of the enum option. 70 | /// 71 | /// # Arguments 72 | /// 73 | /// * `value` - The values that should act as minimal valid value. 74 | pub fn min(mut self, value: i32) -> Self { 75 | self.min = value; 76 | self 77 | } 78 | 79 | /// Set maximum value of the enum option. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `value` - The values that should act as maximal valid value. 84 | pub fn max(mut self, value: i32) -> Self { 85 | self.max = value; 86 | self 87 | } 88 | 89 | /// Set the string values of the option. 90 | /// 91 | /// # Arguments 92 | /// 93 | /// * `values` - The values that should act as the symbolic values. 94 | /// 95 | /// # Examples 96 | /// ```no_run 97 | /// use weechat::config::EnumOptionSettings; 98 | /// 99 | /// let settings = EnumOptionSettings::new("server_buffer") 100 | /// .string_values(vec!["independent", "merged"]); 101 | /// ``` 102 | pub fn string_values(mut self, values: I) -> Self 103 | where 104 | I: IntoIterator, 105 | T: Into, 106 | { 107 | let vec: Vec = values.into_iter().map(Into::into).collect(); 108 | self.string_values = vec.join("|"); 109 | self 110 | } 111 | 112 | /// Set the callback that will run when the value of the option changes. 113 | /// 114 | /// # Arguments 115 | /// 116 | /// * `callback` - The callback that will be run. 117 | /// 118 | /// # Examples 119 | /// ``` 120 | /// use weechat::Weechat; 121 | /// use weechat::config::EnumOptionSettings; 122 | /// 123 | /// let settings = EnumOptionSettings::new("address") 124 | /// .set_change_callback(|weechat, option| { 125 | /// Weechat::print("Option changed"); 126 | /// }); 127 | /// ``` 128 | pub fn set_change_callback( 129 | mut self, 130 | callback: impl FnMut(&Weechat, &EnumOption) + 'static, 131 | ) -> Self { 132 | self.change_cb = Some(Box::new(callback)); 133 | self 134 | } 135 | } 136 | 137 | /// A config option with a enum value. 138 | pub struct EnumOption<'a> { 139 | pub(crate) ptr: *mut t_config_option, 140 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 141 | pub(crate) _phantom: PhantomData<&'a ConfigSection>, 142 | } 143 | 144 | impl EnumOption<'_> { 145 | /// Get the value of the option. 146 | pub fn value(&self) -> i32 { 147 | let weechat = self.get_weechat(); 148 | let config_enum = weechat.get().config_enum.unwrap(); 149 | unsafe { config_enum(self.get_ptr()) } 150 | } 151 | } 152 | 153 | impl FromPtrs for EnumOption<'_> { 154 | fn from_ptrs(option_ptr: *mut t_config_option, weechat_ptr: *mut t_weechat_plugin) -> Self { 155 | EnumOption { ptr: option_ptr, weechat_ptr, _phantom: PhantomData } 156 | } 157 | } 158 | 159 | impl HiddenConfigOptionT for EnumOption<'_> { 160 | fn get_ptr(&self) -> *mut t_config_option { 161 | self.ptr 162 | } 163 | 164 | fn get_weechat(&self) -> Weechat { 165 | Weechat::from_ptr(self.weechat_ptr) 166 | } 167 | } 168 | 169 | impl BaseConfigOption for EnumOption<'_> {} 170 | impl ConfigOptions for EnumOption<'_> {} 171 | -------------------------------------------------------------------------------- /crates/weechat/src/config/integer.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use weechat_sys::{t_config_option, t_weechat_plugin}; 4 | 5 | use crate::{ 6 | config::{ 7 | config_options::{ConfigOptions, FromPtrs, HiddenConfigOptionT}, 8 | BaseConfigOption, ConfigSection, 9 | }, 10 | Weechat, 11 | }; 12 | 13 | type IntegerOptionCallback = Box; 14 | 15 | /// Settings for a new integer option. 16 | #[derive(Default)] 17 | pub struct IntegerOptionSettings { 18 | pub(crate) name: String, 19 | 20 | pub(crate) description: String, 21 | 22 | pub(crate) default_value: i32, 23 | 24 | pub(crate) min: i32, 25 | 26 | pub(crate) max: i32, 27 | 28 | #[cfg(weechat410)] 29 | pub(crate) string_values: String, 30 | 31 | pub(crate) change_cb: Option, 32 | } 33 | 34 | impl IntegerOptionSettings { 35 | /// Create new settings that can be used to create a new integer option. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// * `name` - The name of the new option. 40 | pub fn new>(name: N) -> Self { 41 | IntegerOptionSettings { name: name.into(), ..Default::default() } 42 | } 43 | 44 | /// Set the description of the option. 45 | /// 46 | /// # Arguments 47 | /// 48 | /// * `description` - The description of the new option. 49 | pub fn description>(mut self, description: D) -> Self { 50 | self.description = description.into(); 51 | self 52 | } 53 | 54 | /// Set the default value of the option. 55 | /// 56 | /// This is the value the option will have if it isn't set by the user. If 57 | /// the option is reset, the option will take this value. 58 | /// 59 | /// # Arguments 60 | /// 61 | /// * `value` - The value that should act as the default value. 62 | pub fn default_value>(mut self, value: V) -> Self { 63 | self.default_value = value.into(); 64 | self 65 | } 66 | 67 | /// Set the string values of the option. 68 | /// 69 | /// This setting decides if the integer option should act as an enum taking 70 | /// symbolic values. 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `values` - The values that should act as the symbolic values. 75 | /// 76 | /// # Examples 77 | /// ```no_run 78 | /// use weechat::config::IntegerOptionSettings; 79 | /// 80 | /// let settings = IntegerOptionSettings::new("server_buffer") 81 | /// .string_values(vec!["independent", "merged"]); 82 | /// ``` 83 | #[cfg(weechat410)] 84 | pub fn string_values(mut self, values: I) -> Self 85 | where 86 | I: IntoIterator, 87 | T: Into, 88 | { 89 | let vec: Vec = values.into_iter().map(Into::into).collect(); 90 | self.string_values = vec.join("|"); 91 | self 92 | } 93 | 94 | /// Set minimal value of the integer option. 95 | /// 96 | /// # Arguments 97 | /// 98 | /// * `value` - The values that should act as minimal valid value. 99 | pub fn min(mut self, value: i32) -> Self { 100 | self.min = value; 101 | self 102 | } 103 | 104 | /// Set maximum value of the integer option. 105 | /// 106 | /// # Arguments 107 | /// 108 | /// * `value` - The values that should act as maximal valid value. 109 | pub fn max(mut self, value: i32) -> Self { 110 | self.max = value; 111 | self 112 | } 113 | 114 | /// Set the callback that will run when the value of the option changes. 115 | /// 116 | /// # Arguments 117 | /// 118 | /// * `callback` - The callback that will be run. 119 | /// 120 | /// # Examples 121 | /// 122 | /// ``` 123 | /// use weechat::Weechat; 124 | /// use weechat::config::IntegerOptionSettings; 125 | /// 126 | /// let settings = IntegerOptionSettings::new("server_buffer") 127 | /// .min(0) 128 | /// .max(100) 129 | /// .set_change_callback(|weechat, option| { 130 | /// Weechat::print("Option changed"); 131 | /// }); 132 | /// ``` 133 | pub fn set_change_callback( 134 | mut self, 135 | callback: impl FnMut(&Weechat, &IntegerOption) + 'static, 136 | ) -> Self { 137 | self.change_cb = Some(Box::new(callback)); 138 | self 139 | } 140 | } 141 | 142 | /// A config option with a integer value. 143 | pub struct IntegerOption<'a> { 144 | pub(crate) ptr: *mut t_config_option, 145 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 146 | pub(crate) _phantom: PhantomData<&'a ConfigSection>, 147 | } 148 | 149 | impl IntegerOption<'_> { 150 | /// Get the value of the option. 151 | pub fn value(&self) -> i32 { 152 | let weechat = self.get_weechat(); 153 | let config_integer = weechat.get().config_integer.unwrap(); 154 | unsafe { config_integer(self.get_ptr()) } 155 | } 156 | } 157 | 158 | impl FromPtrs for IntegerOption<'_> { 159 | fn from_ptrs(option_ptr: *mut t_config_option, weechat_ptr: *mut t_weechat_plugin) -> Self { 160 | IntegerOption { ptr: option_ptr, weechat_ptr, _phantom: PhantomData } 161 | } 162 | } 163 | 164 | impl HiddenConfigOptionT for IntegerOption<'_> { 165 | fn get_ptr(&self) -> *mut t_config_option { 166 | self.ptr 167 | } 168 | 169 | fn get_weechat(&self) -> Weechat { 170 | Weechat::from_ptr(self.weechat_ptr) 171 | } 172 | } 173 | 174 | impl BaseConfigOption for IntegerOption<'_> {} 175 | impl ConfigOptions for IntegerOption<'_> {} 176 | -------------------------------------------------------------------------------- /crates/weechat/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! Weechat configuration for plugins. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ```no_run 6 | //! use weechat::Weechat; 7 | //! use weechat::config::{Config, BooleanOptionSettings, ConfigSectionSettings, BooleanOption}; 8 | //! 9 | //! let mut config = Config::new("my_plugin") 10 | //! .expect("Can't create new config"); 11 | //! 12 | //! let server_section_options = ConfigSectionSettings::new("look"); 13 | //! { 14 | //! let mut look_section = config.new_section(server_section_options) 15 | //! .expect("Can't create new section"); 16 | //! 17 | //! let use_colors = BooleanOptionSettings::new("use_colors") 18 | //! .set_change_callback(move |weechat: &Weechat, option: &BooleanOption| {}); 19 | //! 20 | //! let use_colors = look_section.new_boolean_option(use_colors); 21 | //! } 22 | //! 23 | //! config.read().expect("Can't read config"); 24 | //! ``` 25 | 26 | mod boolean; 27 | mod color; 28 | #[allow(clippy::module_inception)] 29 | mod config; 30 | mod config_options; 31 | #[cfg(not(weechat410))] 32 | mod r#enum; 33 | mod integer; 34 | mod section; 35 | mod string; 36 | 37 | #[cfg(not(weechat410))] 38 | pub use crate::config::r#enum::{EnumOption, EnumOptionSettings}; 39 | pub use crate::config::{ 40 | boolean::{BooleanOption, BooleanOptionSettings}, 41 | color::{ColorOption, ColorOptionSettings}, 42 | config::{Conf, Config, ConfigReloadCallback, OptionChanged}, 43 | config_options::{BaseConfigOption, ConfigOptions, OptionType}, 44 | integer::{IntegerOption, IntegerOptionSettings}, 45 | section::{ 46 | ConfigOption, ConfigSection, ConfigSectionSettings, SectionHandle, SectionHandleMut, 47 | SectionReadCallback, SectionWriteCallback, SectionWriteDefaultCallback, 48 | }, 49 | string::{StringOption, StringOptionSettings}, 50 | }; 51 | -------------------------------------------------------------------------------- /crates/weechat/src/config/string.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, marker::PhantomData}; 2 | 3 | use weechat_sys::{t_config_option, t_weechat_plugin}; 4 | 5 | use crate::{ 6 | config::{ 7 | config_options::{ConfigOptions, FromPtrs, HiddenConfigOptionT}, 8 | BaseConfigOption, ConfigSection, 9 | }, 10 | Weechat, 11 | }; 12 | 13 | type StringCheckCb = Box) -> bool>; 14 | type StringChangeCallback = Box; 15 | 16 | /// Settings for a new string option. 17 | #[derive(Default)] 18 | pub struct StringOptionSettings { 19 | pub(crate) name: String, 20 | 21 | pub(crate) description: String, 22 | 23 | pub(crate) default_value: String, 24 | 25 | pub(crate) change_cb: Option, 26 | 27 | pub(crate) check_cb: Option, 28 | } 29 | 30 | impl StringOptionSettings { 31 | /// Create new settings that can be used to create a new string option. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `name` - The name of the new option. 36 | pub fn new>(name: N) -> Self { 37 | StringOptionSettings { name: name.into(), ..Default::default() } 38 | } 39 | 40 | /// Set the description of the option. 41 | /// 42 | /// # Arguments 43 | /// 44 | /// * `description` - The description of the new option. 45 | pub fn description>(mut self, description: D) -> Self { 46 | self.description = description.into(); 47 | self 48 | } 49 | 50 | /// Set the default value of the option. 51 | /// 52 | /// This is the value the option will have if it isn't set by the user. If 53 | /// the option is reset, the option will take this value. 54 | /// 55 | /// # Arguments 56 | /// 57 | /// * `value` - The value that should act as the default value. 58 | pub fn default_value>(mut self, value: V) -> Self { 59 | self.default_value = value.into(); 60 | self 61 | } 62 | 63 | /// Set the callback that will run when the value of the option changes. 64 | /// 65 | /// # Arguments 66 | /// 67 | /// * `callback` - The callback that will be run. 68 | /// 69 | /// # Examples 70 | /// ``` 71 | /// use weechat::Weechat; 72 | /// use weechat::config::StringOptionSettings; 73 | /// 74 | /// let settings = StringOptionSettings::new("address") 75 | /// .set_change_callback(|weechat, option| { 76 | /// Weechat::print("Option changed"); 77 | /// }); 78 | /// ``` 79 | pub fn set_change_callback( 80 | mut self, 81 | callback: impl FnMut(&Weechat, &StringOption) + 'static, 82 | ) -> Self { 83 | self.change_cb = Some(Box::new(callback)); 84 | self 85 | } 86 | 87 | /// Set a callback to check the validity of the string option. 88 | /// 89 | /// # Arguments 90 | /// 91 | /// * `callback` - The callback that will be run. 92 | /// 93 | /// # Examples 94 | /// ``` 95 | /// use weechat::config::StringOptionSettings; 96 | /// 97 | /// let settings = StringOptionSettings::new("address") 98 | /// .set_check_callback(|weechat, option, value| { 99 | /// value.starts_with("http") 100 | /// }); 101 | /// ``` 102 | pub fn set_check_callback( 103 | mut self, 104 | callback: impl FnMut(&Weechat, &StringOption, Cow) -> bool + 'static, 105 | ) -> Self { 106 | self.check_cb = Some(Box::new(callback)); 107 | self 108 | } 109 | } 110 | 111 | /// A config option with a string value. 112 | pub struct StringOption<'a> { 113 | pub(crate) ptr: *mut t_config_option, 114 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 115 | pub(crate) _phantom: PhantomData<&'a ConfigSection>, 116 | } 117 | 118 | impl StringOption<'_> { 119 | /// Get the value of the option. 120 | pub fn value(&self) -> Cow { 121 | let weechat = self.get_weechat(); 122 | let config_string = weechat.get().config_string.unwrap(); 123 | unsafe { 124 | let string = config_string(self.get_ptr()); 125 | CStr::from_ptr(string).to_string_lossy() 126 | } 127 | } 128 | } 129 | 130 | impl FromPtrs for StringOption<'_> { 131 | fn from_ptrs(option_ptr: *mut t_config_option, weechat_ptr: *mut t_weechat_plugin) -> Self { 132 | StringOption { ptr: option_ptr, weechat_ptr, _phantom: PhantomData } 133 | } 134 | } 135 | 136 | impl HiddenConfigOptionT for StringOption<'_> { 137 | fn get_ptr(&self) -> *mut t_config_option { 138 | self.ptr 139 | } 140 | 141 | fn get_weechat(&self) -> Weechat { 142 | Weechat::from_ptr(self.weechat_ptr) 143 | } 144 | } 145 | 146 | impl BaseConfigOption for StringOption<'_> {} 147 | impl ConfigOptions for StringOption<'_> {} 148 | -------------------------------------------------------------------------------- /crates/weechat/src/executor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::VecDeque, 3 | panic, 4 | sync::{Arc, Mutex}, 5 | }; 6 | 7 | pub use async_task::{Runnable, Task}; 8 | use futures::future::{BoxFuture, Future}; 9 | use pipe_channel::{channel, Receiver, Sender}; 10 | 11 | use crate::{ 12 | hooks::{FdHook, FdHookCallback, FdHookMode}, 13 | Weechat, 14 | }; 15 | 16 | static mut _EXECUTOR: Option = None; 17 | 18 | type BufferName = String; 19 | 20 | type Job = Runnable; 21 | struct BufferJob(Runnable, BufferName); 22 | 23 | impl BufferJob { 24 | fn run(self) -> bool { 25 | self.0.run() 26 | } 27 | 28 | fn cancel(self) { 29 | drop(self) 30 | } 31 | 32 | fn tag(&self) -> &BufferName { 33 | &self.1 34 | } 35 | } 36 | 37 | enum ExecutorJob { 38 | Job(Job), 39 | BufferJob(BufferJob), 40 | } 41 | 42 | type FutureQueue = Arc>>; 43 | 44 | #[derive(Clone)] 45 | pub struct WeechatExecutor { 46 | _hook: Arc>>>>, 47 | sender: Arc>>, 48 | futures: FutureQueue, 49 | non_local_futures: Arc>>>, 50 | } 51 | 52 | impl FdHookCallback for WeechatExecutor { 53 | type FdObject = Receiver<()>; 54 | 55 | fn callback(&mut self, _weechat: &Weechat, receiver: &mut Receiver<()>) { 56 | if receiver.recv().is_err() { 57 | return; 58 | } 59 | 60 | let future = self.futures.lock().unwrap().pop_front(); 61 | 62 | // Run a local future if there is one. 63 | if let Some(task) = future { 64 | match task { 65 | ExecutorJob::Job(t) => { 66 | let _ = panic::catch_unwind(|| t.run()); 67 | } 68 | ExecutorJob::BufferJob(t) => { 69 | let weechat = unsafe { Weechat::weechat() }; 70 | let buffer_name = t.tag(); 71 | 72 | let buffer = weechat.buffer_search("==", buffer_name); 73 | 74 | if buffer.is_some() { 75 | let _ = panic::catch_unwind(|| t.run()); 76 | } else { 77 | t.cancel() 78 | } 79 | } 80 | } 81 | } 82 | 83 | let future = self.non_local_futures.lock().unwrap().pop_front(); 84 | // Spawn a future if there was one sent from another thread. 85 | if let Some(future) = future { 86 | self.spawn_local(future).detach(); 87 | } 88 | } 89 | } 90 | 91 | impl WeechatExecutor { 92 | fn new() -> Self { 93 | let (sender, receiver) = channel(); 94 | let sender = Arc::new(Mutex::new(sender)); 95 | let queue = Arc::new(Mutex::new(VecDeque::new())); 96 | let non_local = Arc::new(Mutex::new(VecDeque::new())); 97 | 98 | let executor = WeechatExecutor { 99 | #[allow(clippy::arc_with_non_send_sync)] 100 | _hook: Arc::new(Mutex::new(None)), 101 | sender, 102 | futures: queue, 103 | non_local_futures: non_local, 104 | }; 105 | 106 | let hook = FdHook::new(receiver, FdHookMode::Read, executor.clone()) 107 | .expect("Can't create executor FD hook"); 108 | 109 | *executor._hook.lock().unwrap() = Some(hook); 110 | 111 | executor 112 | } 113 | 114 | pub fn spawn_local(&self, future: F) -> Task 115 | where 116 | F: Future + 'static, 117 | F::Output: 'static, 118 | { 119 | let sender = Arc::downgrade(&self.sender); 120 | let queue = Arc::downgrade(&self.futures); 121 | 122 | let schedule = move |runnable| { 123 | let sender = sender.upgrade(); 124 | let queue = queue.upgrade(); 125 | 126 | if let Some(q) = queue { 127 | let sender = sender.expect("Futures queue exists but the channel got dropped"); 128 | let mut weechat_notify = 129 | sender.lock().expect("Weechat notification sender lock is poisoned"); 130 | 131 | let mut queue = 132 | q.lock().expect("Lock of the future queue of the Weechat executor is poisoned"); 133 | 134 | queue.push_back(ExecutorJob::Job(runnable)); 135 | weechat_notify.send(()).expect("Can't notify Weechat to run a future"); 136 | } 137 | }; 138 | 139 | let (runnable, task) = async_task::spawn_local(future, schedule); 140 | 141 | runnable.schedule(); 142 | 143 | task 144 | } 145 | 146 | pub fn free() { 147 | unsafe { 148 | #[allow(static_mut_refs)] 149 | _EXECUTOR.take(); 150 | } 151 | } 152 | 153 | pub fn start() { 154 | let executor = WeechatExecutor::new(); 155 | #[allow(static_mut_refs)] 156 | unsafe { 157 | _EXECUTOR = Some(executor); 158 | } 159 | } 160 | 161 | /// Spawn a local Weechat future from the non-main thread. 162 | pub fn spawn_from_non_main(future: F) 163 | where 164 | F: Future + Send + 'static, 165 | { 166 | #[allow(static_mut_refs)] 167 | let executor = unsafe { _EXECUTOR.as_ref().expect("Executor wasn't started") }; 168 | 169 | let future = Box::pin(future); 170 | let mut queue = executor.non_local_futures.lock().unwrap(); 171 | queue.push_back(future); 172 | executor 173 | .sender 174 | .lock() 175 | .unwrap() 176 | .send(()) 177 | .expect("Can't notify Weechat to spawn a non-local future"); 178 | } 179 | 180 | /// Spawn a future that will run on the Weechat main loop. 181 | pub fn spawn(future: F) -> Option> 182 | where 183 | F: Future + 'static, 184 | F::Output: 'static, 185 | { 186 | #[allow(static_mut_refs)] 187 | let executor = unsafe { _EXECUTOR.as_ref() }; 188 | executor.map(|executor| executor.spawn_local(future)) 189 | } 190 | 191 | pub(crate) fn spawn_buffer_cb(buffer_name: String, future: F) -> Task 192 | where 193 | F: Future + 'static, 194 | F::Output: 'static, 195 | { 196 | #[allow(static_mut_refs)] 197 | let executor = unsafe { _EXECUTOR.as_ref().expect("Executor wasn't started") }; 198 | 199 | let sender = Arc::downgrade(&executor.sender); 200 | let queue = Arc::downgrade(&executor.futures); 201 | 202 | let schedule = move |runnable| { 203 | let sender = sender.upgrade(); 204 | let queue = queue.upgrade(); 205 | 206 | if let Some(q) = queue { 207 | let sender = sender.expect("Futures queue exists but the channel got dropped"); 208 | let mut weechat_notify = 209 | sender.lock().expect("Weechat notification sender lock is poisoned"); 210 | 211 | let mut queue = 212 | q.lock().expect("Lock of the future queue of the Weechat executor is poisoned"); 213 | 214 | queue.push_back(ExecutorJob::BufferJob(BufferJob(runnable, buffer_name.clone()))); 215 | weechat_notify.send(()).expect("Can't notify Weechat to run a future"); 216 | } 217 | }; 218 | 219 | let (runnable, task) = async_task::spawn_local(future, schedule); 220 | 221 | runnable.schedule(); 222 | 223 | task 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /crates/weechat/src/hashtable.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, ffi::c_void, os::raw::c_char}; 2 | 3 | use weechat_sys::{t_hashtable, WEECHAT_HASHTABLE_STRING}; 4 | 5 | use crate::{LossyCString, Weechat}; 6 | 7 | impl Weechat { 8 | pub(crate) fn hashmap_to_weechat(&self, hashmap: HashMap<&str, &str>) -> *mut t_hashtable { 9 | let hashtable_new = self.get().hashtable_new.unwrap(); 10 | 11 | let table_type: *const c_char = WEECHAT_HASHTABLE_STRING as *const _ as *const c_char; 12 | 13 | let hashtable = unsafe { hashtable_new(8, table_type, table_type, None, None) }; 14 | 15 | for (key, value) in hashmap { 16 | let key = LossyCString::new(key); 17 | let value = LossyCString::new(value); 18 | 19 | unsafe { 20 | self.get().hashtable_set.unwrap()( 21 | hashtable, 22 | key.as_ptr() as *const c_void, 23 | value.as_ptr() as *const c_void, 24 | ); 25 | } 26 | } 27 | 28 | hashtable 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/weechat/src/hdata.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::HashMap, 4 | ffi::{c_void, CStr}, 5 | os::raw::c_char, 6 | }; 7 | 8 | use weechat_sys::t_hdata; 9 | 10 | use crate::{LossyCString, Weechat}; 11 | 12 | impl Weechat { 13 | pub(crate) unsafe fn hdata_get(&self, name: &str) -> *mut t_hdata { 14 | let hdata_get = self.get().hdata_get.unwrap(); 15 | 16 | let name = LossyCString::new(name); 17 | 18 | hdata_get(self.ptr, name.as_ptr()) 19 | } 20 | 21 | pub(crate) unsafe fn hdata_pointer( 22 | &self, 23 | hdata: *mut t_hdata, 24 | pointer: *mut c_void, 25 | name: &str, 26 | ) -> *mut c_void { 27 | let hdata_pointer = self.get().hdata_pointer.unwrap(); 28 | let name = LossyCString::new(name); 29 | 30 | hdata_pointer(hdata, pointer, name.as_ptr()) 31 | } 32 | 33 | pub(crate) unsafe fn hdata_integer( 34 | &self, 35 | hdata: *mut t_hdata, 36 | pointer: *mut c_void, 37 | name: &str, 38 | ) -> i32 { 39 | let hdata_integer = self.get().hdata_integer.unwrap(); 40 | let name = LossyCString::new(name); 41 | 42 | hdata_integer(hdata, pointer, name.as_ptr()) 43 | } 44 | 45 | pub(crate) unsafe fn hdata_time( 46 | &self, 47 | hdata: *mut t_hdata, 48 | pointer: *mut c_void, 49 | name: &str, 50 | ) -> isize { 51 | let hdata_time = self.get().hdata_time.unwrap(); 52 | let name = LossyCString::new(name); 53 | 54 | hdata_time(hdata, pointer, name.as_ptr()) as _ 55 | } 56 | 57 | pub(crate) unsafe fn hdata_char( 58 | &self, 59 | hdata: *mut t_hdata, 60 | pointer: *mut c_void, 61 | name: &str, 62 | ) -> c_char { 63 | let hdata_char = self.get().hdata_char.unwrap(); 64 | let name = LossyCString::new(name); 65 | 66 | hdata_char(hdata, pointer, name.as_ptr()) 67 | } 68 | 69 | pub(crate) unsafe fn hdata_var_array_size( 70 | &self, 71 | hdata: *mut t_hdata, 72 | pointer: *mut c_void, 73 | name: &str, 74 | ) -> i32 { 75 | let hdata_get_var_array_size = self.get().hdata_get_var_array_size.unwrap(); 76 | let name = LossyCString::new(name); 77 | 78 | hdata_get_var_array_size(hdata, pointer, name.as_ptr()) 79 | } 80 | 81 | pub(crate) unsafe fn hdata_move( 82 | &self, 83 | hdata: *mut t_hdata, 84 | pointer: *mut c_void, 85 | offset: i32, 86 | ) -> *mut c_void { 87 | let hdata_move = self.get().hdata_move.unwrap(); 88 | hdata_move(hdata, pointer, offset) 89 | } 90 | 91 | pub(crate) unsafe fn hdata_string( 92 | &self, 93 | hdata: *mut t_hdata, 94 | pointer: *mut c_void, 95 | name: &str, 96 | ) -> Cow { 97 | let hdata_string = self.get().hdata_string.unwrap(); 98 | let name = LossyCString::new(name); 99 | 100 | let string_ptr = hdata_string(hdata, pointer, name.as_ptr()); 101 | CStr::from_ptr(string_ptr).to_string_lossy() 102 | } 103 | 104 | pub(crate) unsafe fn hdata_update( 105 | &self, 106 | hdata: *mut t_hdata, 107 | pointer: *mut c_void, 108 | hashmap: HashMap<&str, &str>, 109 | ) -> i32 { 110 | let hdata_update = self.get().hdata_update.unwrap(); 111 | 112 | let hashtable = self.hashmap_to_weechat(hashmap); 113 | let ret = hdata_update(hdata, pointer, hashtable); 114 | self.get().hashtable_free.unwrap()(hashtable); 115 | ret 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/bar.rs: -------------------------------------------------------------------------------- 1 | //! Bar items are used to display status information in Weechat. 2 | use core::ptr; 3 | use std::os::raw::c_void; 4 | 5 | use libc::c_char; 6 | use weechat_sys::{t_gui_bar_item, t_gui_buffer, t_gui_window, t_hashtable, t_weechat_plugin}; 7 | 8 | use crate::{buffer::Buffer, LossyCString, Weechat}; 9 | 10 | /// Trait for the bar item callback 11 | /// 12 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 13 | /// be passed to the callback implement this over your struct. 14 | pub trait BarItemCallback: 'static { 15 | /// The callback that should be called after the bar items 16 | /// is marked to be updated. 17 | /// 18 | /// Should return a string that will be displayed by the bar item. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `weeechat` - A reference to the weechat context. 23 | /// 24 | /// * `buffer` - The currently visible buffer. 25 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer) -> String; 26 | } 27 | 28 | impl String + 'static> BarItemCallback for T { 29 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer) -> String { 30 | self(weechat, buffer) 31 | } 32 | } 33 | 34 | struct BarItemCbData { 35 | callback: Box, 36 | weechat_ptr: *mut t_weechat_plugin, 37 | } 38 | 39 | /// A handle to a bar item. The bar item is automatically removed when the 40 | /// object is dropped. 41 | pub struct BarItem { 42 | name: String, 43 | ptr: *mut t_gui_bar_item, 44 | weechat: *mut t_weechat_plugin, 45 | _data: Box, 46 | } 47 | 48 | impl Drop for BarItem { 49 | fn drop(&mut self) { 50 | let weechat = Weechat::from_ptr(self.weechat); 51 | let bar_item_remove = weechat.get().bar_item_remove.unwrap(); 52 | unsafe { bar_item_remove(self.ptr) }; 53 | } 54 | } 55 | 56 | impl BarItem { 57 | /// Create a new bar item that can be added by a user. 58 | /// 59 | /// # Arguments 60 | /// 61 | /// * `name` - The name of the new bar item. 62 | /// 63 | /// * `callback` - The callback that should be called after the bar items is 64 | /// marked to be updated. 65 | /// 66 | /// # Panics 67 | /// 68 | /// Panics if the method is not called from the main Weechat thread. 69 | /// 70 | /// # Example 71 | /// ```no_run 72 | /// # use weechat::Weechat; 73 | /// # use weechat::buffer::Buffer; 74 | /// # use weechat::hooks::BarItem; 75 | /// let item = BarItem::new("buffer_plugin", |weechat:&Weechat, 76 | /// buffer: &Buffer| { 77 | /// "rust/sample".to_owned() 78 | /// }); 79 | /// ``` 80 | // TODO: Provide window object, the callback should accept a Window object 81 | // wrapping a t_gui_window 82 | // 83 | // TODO: If we're going to allow bar items to be searched for like we do for 84 | // buffers, we need to do something about the multiple ownership that may 85 | // come from this. 86 | pub fn new(name: &str, callback: impl BarItemCallback) -> Result { 87 | unsafe extern "C" fn c_item_cb( 88 | pointer: *const c_void, 89 | _data: *mut c_void, 90 | _bar_item: *mut t_gui_bar_item, 91 | _window: *mut t_gui_window, 92 | buffer: *mut t_gui_buffer, 93 | _extra_info: *mut t_hashtable, 94 | ) -> *mut c_char { 95 | let data: &mut BarItemCbData = { &mut *(pointer as *mut BarItemCbData) }; 96 | let weechat = Weechat::from_ptr(data.weechat_ptr); 97 | let buffer = weechat.buffer_from_ptr(buffer); 98 | 99 | let cb_trait = &mut data.callback; 100 | 101 | let ret = cb_trait.callback(&weechat, &buffer); 102 | 103 | // Weechat wants a malloc'ed string 104 | libc::strdup(LossyCString::new(ret).as_ptr()) 105 | } 106 | Weechat::check_thread(); 107 | let weechat = unsafe { Weechat::weechat() }; 108 | 109 | let data = 110 | Box::new(BarItemCbData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 111 | 112 | let data_ref = Box::leak(data); 113 | let bar_item_new = weechat.get().bar_item_new.unwrap(); 114 | 115 | let bar_item_name = LossyCString::new(name); 116 | 117 | let bar_item_ptr = unsafe { 118 | bar_item_new( 119 | weechat.ptr, 120 | bar_item_name.as_ptr(), 121 | Some(c_item_cb), 122 | data_ref as *const _ as *const c_void, 123 | ptr::null_mut(), 124 | ) 125 | }; 126 | 127 | let cb_data = unsafe { Box::from_raw(data_ref) }; 128 | 129 | if bar_item_ptr.is_null() { 130 | return Err(()); 131 | } 132 | 133 | Ok(BarItem { 134 | name: name.to_owned(), 135 | ptr: bar_item_ptr, 136 | weechat: weechat.ptr, 137 | _data: cb_data, 138 | }) 139 | } 140 | 141 | /// Update the content of the bar item, by calling its build callback. 142 | pub fn update(&self) { 143 | Weechat::bar_item_update(&self.name); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/commands.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, os::raw::c_void, ptr}; 2 | 3 | use libc::{c_char, c_int}; 4 | use weechat_sys::{t_gui_buffer, t_weechat_plugin, WEECHAT_RC_OK}; 5 | 6 | use super::Hook; 7 | use crate::{buffer::Buffer, Args, LossyCString, ReturnCode, Weechat}; 8 | 9 | /// Hook for a weechat command, the command is removed when the object is 10 | /// dropped. 11 | pub struct Command { 12 | _hook: Hook, 13 | _hook_data: Box, 14 | } 15 | 16 | /// Trait for the command callback 17 | /// 18 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 19 | /// be passed to the callback implement this over your struct. 20 | pub trait CommandCallback { 21 | /// Callback that will be called when the command is executed. 22 | /// 23 | /// # Arguments 24 | /// 25 | /// * `weechat` - A Weechat context. 26 | /// 27 | /// * `buffer` - The buffer that received the command. 28 | /// 29 | /// * `arguments` - The arguments that were passed to the command, this will 30 | /// include the command as the first argument. 31 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer, arguments: Args); 32 | } 33 | 34 | impl CommandCallback for T { 35 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer, arguments: Args) { 36 | self(weechat, buffer, arguments) 37 | } 38 | } 39 | 40 | #[derive(Default)] 41 | /// Description for a new Weechat command that should be created. 42 | /// 43 | /// The fields of this struct accept the same string formats that are described 44 | /// in the Weechat API documentation. 45 | pub struct CommandSettings { 46 | /// Name of the command. 47 | name: String, 48 | /// Description for the command (displayed with `/help command`) 49 | description: String, 50 | /// Arguments for the command (displayed with `/help command`) 51 | arguments: Vec, 52 | /// Description for the command arguments (displayed with `/help command`) 53 | argument_description: String, 54 | /// Completion template for the command. 55 | completion: Vec, 56 | } 57 | 58 | impl CommandSettings { 59 | /// Create new command settings. 60 | /// 61 | /// This describes how a command will be created. 62 | /// 63 | /// #Arguments 64 | /// 65 | /// * `name` - The name that the section should get. 66 | pub fn new>(name: P) -> Self { 67 | CommandSettings { name: name.into(), ..Default::default() } 68 | } 69 | 70 | /// Set the description of the command. 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `description` - The description of the command. 75 | pub fn description>(mut self, description: D) -> Self { 76 | self.description = description.into(); 77 | self 78 | } 79 | 80 | /// Add an argument to the command. 81 | /// 82 | /// Multiple arguments can be added to a command. See the `Command` 83 | /// documentation for an example of this. 84 | /// 85 | /// # Arguments 86 | /// 87 | /// * `argument` - The argument that should be added. 88 | pub fn add_argument>(mut self, argument: T) -> Self { 89 | self.arguments.push(argument.into()); 90 | self 91 | } 92 | 93 | /// Set the description of the arguments. 94 | /// 95 | /// # Arguments 96 | /// 97 | /// * `description` - The argument description that should be set for the 98 | /// command. 99 | pub fn arguments_description>(mut self, description: T) -> Self { 100 | self.argument_description = description.into(); 101 | self 102 | } 103 | 104 | /// Add a completion definition to the command. 105 | /// 106 | /// Multiple arguments can be added to a command. See the `Command` 107 | /// documentation for an example of this. 108 | /// 109 | /// # Arguments 110 | /// 111 | /// * `completion` - The completion that should be added to the command. 112 | pub fn add_completion>(mut self, completion: T) -> Self { 113 | self.completion.push(completion.into()); 114 | self 115 | } 116 | } 117 | 118 | struct CommandHookData { 119 | callback: Box, 120 | weechat_ptr: *mut t_weechat_plugin, 121 | } 122 | 123 | /// Hook for a weechat command, the hook is removed when the object is dropped. 124 | pub struct CommandRun { 125 | _hook: Hook, 126 | _hook_data: Box, 127 | } 128 | 129 | /// Trait for the command-run callback 130 | /// 131 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 132 | /// be passed to the callback implement this over your struct. 133 | pub trait CommandRunCallback { 134 | /// Callback that will be called when the command is ran. 135 | /// 136 | /// Should return a code signaling if further command callbacks should be 137 | /// ran or if the command should be "eaten" by this callback. 138 | /// 139 | /// # Arguments 140 | /// 141 | /// * `weechat` - A Weechat context. 142 | /// 143 | /// * `buffer` - The buffer that received the command. 144 | /// 145 | /// * `command` - The full command that was executed, including its 146 | /// arguments. 147 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer, command: Cow) -> ReturnCode; 148 | } 149 | 150 | impl) -> ReturnCode + 'static> CommandRunCallback for T { 151 | fn callback(&mut self, weechat: &Weechat, buffer: &Buffer, command: Cow) -> ReturnCode { 152 | self(weechat, buffer, command) 153 | } 154 | } 155 | 156 | struct CommandRunHookData { 157 | callback: Box, 158 | weechat_ptr: *mut t_weechat_plugin, 159 | } 160 | 161 | impl CommandRun { 162 | /// Override an existing Weechat command. 163 | /// 164 | /// # Arguments 165 | /// 166 | /// * `command` - The command to override (wildcard `*` is allowed). 167 | /// 168 | /// * `callback` - The function that will be called when the command is run. 169 | /// 170 | /// # Panics 171 | /// 172 | /// Panics if the method is not called from the main Weechat thread. 173 | /// 174 | /// # Example 175 | /// ```no_run 176 | /// # use std::borrow::Cow; 177 | /// # use weechat::{Weechat, ReturnCode}; 178 | /// # use weechat::hooks::CommandRun; 179 | /// # use weechat::buffer::Buffer; 180 | /// 181 | /// let buffer_command = CommandRun::new( 182 | /// "2000|/buffer *", 183 | /// |_: &Weechat, _: &Buffer, _: Cow| ReturnCode::OkEat, 184 | /// ) 185 | /// .expect("Can't override buffer command"); 186 | /// ``` 187 | pub fn new(command: &str, callback: impl CommandRunCallback + 'static) -> Result { 188 | unsafe extern "C" fn c_hook_cb( 189 | pointer: *const c_void, 190 | _data: *mut c_void, 191 | buffer: *mut t_gui_buffer, 192 | command: *const std::os::raw::c_char, 193 | ) -> c_int { 194 | let hook_data: &mut CommandRunHookData = { &mut *(pointer as *mut CommandRunHookData) }; 195 | let cb = &mut hook_data.callback; 196 | 197 | let weechat = Weechat::from_ptr(hook_data.weechat_ptr); 198 | let buffer = weechat.buffer_from_ptr(buffer); 199 | let command = CStr::from_ptr(command).to_string_lossy(); 200 | 201 | cb.callback(&weechat, &buffer, command) as isize as i32 202 | } 203 | 204 | Weechat::check_thread(); 205 | let weechat = unsafe { Weechat::weechat() }; 206 | 207 | let data = 208 | Box::new(CommandRunHookData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 209 | 210 | let data_ref = Box::leak(data); 211 | let hook_command_run = weechat.get().hook_command_run.unwrap(); 212 | 213 | let command = LossyCString::new(command); 214 | 215 | let hook_ptr = unsafe { 216 | hook_command_run( 217 | weechat.ptr, 218 | command.as_ptr(), 219 | Some(c_hook_cb), 220 | data_ref as *const _ as *const c_void, 221 | ptr::null_mut(), 222 | ) 223 | }; 224 | let hook_data = unsafe { Box::from_raw(data_ref) }; 225 | 226 | if hook_ptr.is_null() { 227 | Err(()) 228 | } else { 229 | let hook = Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }; 230 | 231 | Ok(CommandRun { _hook: hook, _hook_data: hook_data }) 232 | } 233 | } 234 | } 235 | 236 | impl Command { 237 | /// Create a new Weechat command. 238 | /// 239 | /// Returns the hook of the command. The command is unhooked if the hook is 240 | /// dropped. 241 | /// 242 | /// # Arguments 243 | /// 244 | /// * `command_settings` - Settings for the new command. 245 | /// 246 | /// * `callback` - The callback that will be called if the command is run. 247 | /// 248 | /// ```no_run 249 | /// # use weechat::{Weechat, Args}; 250 | /// # use weechat::hooks::{Command, CommandSettings}; 251 | /// # use weechat::buffer::{Buffer}; 252 | /// let settings = CommandSettings::new("irc") 253 | /// .description("IRC chat protocol command.") 254 | /// .add_argument("server add [:]") 255 | /// .add_argument("server delete|list|listfull ") 256 | /// .add_argument("connect ") 257 | /// .add_argument("disconnect ") 258 | /// .add_argument("reconnect ") 259 | /// .add_argument("help []") 260 | /// .arguments_description( 261 | /// " server: List, add, or remove IRC servers. 262 | /// connect: Connect to a IRC server. 263 | /// disconnect: Disconnect from one or all IRC servers. 264 | /// reconnect: Reconnect to server(s). 265 | /// help: Show detailed command help.\n 266 | /// Use /irc [command] help to find out more.\n", 267 | /// ) 268 | /// .add_completion("server |add|delete|list|listfull") 269 | /// .add_completion("connect") 270 | /// .add_completion("disconnect") 271 | /// .add_completion("reconnect") 272 | /// .add_completion("help server|connect|disconnect|reconnect"); 273 | /// 274 | /// let command = Command::new( 275 | /// settings, 276 | /// |_: &Weechat, buffer: &Buffer, args: Args| { 277 | /// buffer.print(&format!("Command called with args {:?}", args)); 278 | /// } 279 | /// ).expect("Can't create command"); 280 | /// ``` 281 | pub fn new( 282 | command_settings: CommandSettings, 283 | callback: impl CommandCallback + 'static, 284 | ) -> Result { 285 | unsafe extern "C" fn c_hook_cb( 286 | pointer: *const c_void, 287 | _data: *mut c_void, 288 | buffer: *mut t_gui_buffer, 289 | argc: i32, 290 | argv: *mut *mut c_char, 291 | _argv_eol: *mut *mut c_char, 292 | ) -> c_int { 293 | let hook_data: &mut CommandHookData = { &mut *(pointer as *mut CommandHookData) }; 294 | let weechat = Weechat::from_ptr(hook_data.weechat_ptr); 295 | let buffer = weechat.buffer_from_ptr(buffer); 296 | let cb = &mut hook_data.callback; 297 | let args = Args::new(argc, argv); 298 | 299 | cb.callback(&weechat, &buffer, args); 300 | 301 | WEECHAT_RC_OK 302 | } 303 | 304 | Weechat::check_thread(); 305 | let weechat = unsafe { Weechat::weechat() }; 306 | 307 | let name = LossyCString::new(command_settings.name); 308 | let description = LossyCString::new(command_settings.description); 309 | let args = LossyCString::new(command_settings.arguments.join("||")); 310 | let args_description = LossyCString::new(command_settings.argument_description); 311 | let completion = LossyCString::new(command_settings.completion.join("||")); 312 | 313 | let data = 314 | Box::new(CommandHookData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 315 | 316 | let data_ref = Box::leak(data); 317 | 318 | let hook_command = weechat.get().hook_command.unwrap(); 319 | let hook_ptr = unsafe { 320 | hook_command( 321 | weechat.ptr, 322 | name.as_ptr(), 323 | description.as_ptr(), 324 | args.as_ptr(), 325 | args_description.as_ptr(), 326 | completion.as_ptr(), 327 | Some(c_hook_cb), 328 | data_ref as *const _ as *const c_void, 329 | ptr::null_mut(), 330 | ) 331 | }; 332 | let hook_data = unsafe { Box::from_raw(data_ref) }; 333 | 334 | let hook = Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }; 335 | 336 | if hook_ptr.is_null() { 337 | Err(()) 338 | } else { 339 | Ok(Command { _hook: hook, _hook_data: hook_data }) 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/completion.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, os::raw::c_void, ptr}; 2 | 3 | use libc::{c_char, c_int}; 4 | use weechat_sys::{ 5 | t_gui_buffer, t_gui_completion, t_weechat_plugin, WEECHAT_RC_ERROR, WEECHAT_RC_OK, 6 | }; 7 | 8 | use crate::{buffer::Buffer, hooks::Hook, LossyCString, Weechat}; 9 | 10 | /// A handle to a completion item. 11 | pub struct Completion { 12 | weechat_ptr: *mut t_weechat_plugin, 13 | ptr: *mut t_gui_completion, 14 | } 15 | 16 | /// Trait for the completion callback. 17 | /// 18 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 19 | pub trait CompletionCallback { 20 | /// Callback that will be called if when a completion is requested. 21 | /// 22 | /// # Arguments 23 | /// 24 | /// * `weechat` - A Weechat context. 25 | /// 26 | /// * `buffer` - The currently active buffer that requested the completion 27 | /// to run. 28 | /// 29 | /// * `completion_name` - The name of the completion. 30 | /// 31 | /// * `completion` - The completion object that should be populated with 32 | /// completion words by the callback. 33 | fn callback( 34 | &mut self, 35 | weechat: &Weechat, 36 | buffer: &Buffer, 37 | completion_name: Cow, 38 | completion: &Completion, 39 | ) -> Result<(), ()>; 40 | } 41 | 42 | impl, &Completion) -> Result<(), ()> + 'static> 43 | CompletionCallback for T 44 | { 45 | fn callback( 46 | &mut self, 47 | weechat: &Weechat, 48 | buffer: &Buffer, 49 | completion_name: Cow, 50 | completion: &Completion, 51 | ) -> Result<(), ()> { 52 | self(weechat, buffer, completion_name, completion) 53 | } 54 | } 55 | 56 | /// The positions an entry can be added to a completion list. 57 | #[derive(Clone, Copy)] 58 | pub enum CompletionPosition { 59 | /// Insert the item in a way that keeps the list sorted. 60 | Sorted, 61 | /// Insert the item at the beginning of the list. 62 | Beginning, 63 | /// Insert the item at the end of the list. 64 | End, 65 | } 66 | 67 | impl CompletionPosition { 68 | pub(crate) fn value(&self) -> &str { 69 | match self { 70 | CompletionPosition::Sorted => "sort", 71 | CompletionPosition::Beginning => "beginning", 72 | CompletionPosition::End => "end", 73 | } 74 | } 75 | } 76 | 77 | impl Completion { 78 | pub(crate) fn from_raw( 79 | weechat: *mut t_weechat_plugin, 80 | completion: *mut t_gui_completion, 81 | ) -> Completion { 82 | Completion { weechat_ptr: weechat, ptr: completion } 83 | } 84 | 85 | /// Add a word for completion, keeping the list sorted. 86 | pub fn add(&self, word: &str) { 87 | self.add_with_options(word, false, CompletionPosition::Sorted) 88 | } 89 | 90 | /// Get the command used in the completion. 91 | pub fn base_command(&self) -> Option> { 92 | self.get_string("base_command") 93 | } 94 | 95 | /// Get the word that is being completed. 96 | pub fn base_word(&self) -> Option> { 97 | self.get_string("base_word") 98 | } 99 | 100 | /// Get the command arguments including the base word. 101 | pub fn arguments(&self) -> Option> { 102 | self.get_string("args") 103 | } 104 | 105 | fn get_string(&self, property_name: &str) -> Option> { 106 | let weechat = Weechat::from_ptr(self.weechat_ptr); 107 | 108 | let get_string = weechat.get().hook_completion_get_string.unwrap(); 109 | 110 | let property_name = LossyCString::new(property_name); 111 | 112 | unsafe { 113 | let ret = get_string(self.ptr, property_name.as_ptr()); 114 | 115 | if ret.is_null() { 116 | None 117 | } else { 118 | Some(CStr::from_ptr(ret).to_string_lossy()) 119 | } 120 | } 121 | } 122 | 123 | /// Add a word to the completion giving the position and whether the word is 124 | /// a nick. 125 | /// 126 | /// # Arguments 127 | /// 128 | /// * `word` - The word that should be added to the completion. 129 | /// 130 | /// * `is_nick` - Set if the word is a nick. 131 | /// 132 | /// * `position` - Set the position where the nick should be added to. 133 | pub fn add_with_options(&self, word: &str, is_nick: bool, position: CompletionPosition) { 134 | let weechat = Weechat::from_ptr(self.weechat_ptr); 135 | 136 | let hook_completion_list_add = weechat.get().hook_completion_list_add.unwrap(); 137 | 138 | let word = LossyCString::new(word); 139 | let method = LossyCString::new(position.value()); 140 | 141 | unsafe { 142 | hook_completion_list_add(self.ptr, word.as_ptr(), is_nick as i32, method.as_ptr()); 143 | } 144 | } 145 | } 146 | 147 | /// Hook for a completion item, the hook is removed when the object is dropped. 148 | pub struct CompletionHook { 149 | _hook: Hook, 150 | _hook_data: Box, 151 | } 152 | 153 | struct CompletionHookData { 154 | #[allow(clippy::type_complexity)] 155 | callback: Box, 156 | weechat_ptr: *mut t_weechat_plugin, 157 | } 158 | 159 | impl CompletionHook { 160 | /// Create a new completion 161 | /// 162 | /// # Arguments 163 | /// 164 | /// * `name` - The name of the new completion. After this is created the can 165 | /// be used as `%(name)` when creating commands. 166 | /// 167 | /// * `description` - The description of the new completion. 168 | /// 169 | /// * `callback` - A function that will be called when the completion is 170 | /// used, the callback must populate the candidates for the completion. 171 | /// 172 | /// # Example 173 | /// ```no_run 174 | /// # use std::borrow::Cow; 175 | /// # use std::collections::HashSet; 176 | /// # use weechat::Weechat; 177 | /// # use weechat::buffer::Buffer; 178 | /// # use weechat::hooks::{CompletionCallback, CompletionHook, Completion, CompletionPosition}; 179 | /// 180 | /// pub struct MyMap { 181 | /// server_names: HashSet, 182 | /// } 183 | /// 184 | /// impl CompletionCallback for MyMap { 185 | /// fn callback(&mut self, _: &Weechat, _: &Buffer, _: Cow, completion: &Completion) -> Result<(), ()> { 186 | /// for server_name in &self.server_names { 187 | /// completion.add_with_options(server_name, false, CompletionPosition::Sorted); 188 | /// } 189 | /// Ok(()) 190 | /// } 191 | /// } 192 | /// 193 | /// let servers = MyMap { server_names: HashSet::new() }; 194 | /// 195 | /// let completion = CompletionHook::new( 196 | /// "matrix_servers", 197 | /// "Completion for the list of added Matrix servers", 198 | /// servers, 199 | /// ).unwrap(); 200 | /// ``` 201 | pub fn new( 202 | completion_item: &str, 203 | description: &str, 204 | callback: impl CompletionCallback + 'static, 205 | ) -> Result { 206 | unsafe extern "C" fn c_hook_cb( 207 | pointer: *const c_void, 208 | _data: *mut c_void, 209 | completion_item: *const c_char, 210 | buffer: *mut t_gui_buffer, 211 | completion: *mut t_gui_completion, 212 | ) -> c_int { 213 | let hook_data: &mut CompletionHookData = { &mut *(pointer as *mut CompletionHookData) }; 214 | let cb = &mut hook_data.callback; 215 | let weechat = Weechat::from_ptr(hook_data.weechat_ptr); 216 | let buffer = weechat.buffer_from_ptr(buffer); 217 | 218 | let completion_item = CStr::from_ptr(completion_item).to_string_lossy(); 219 | 220 | let ret = cb.callback( 221 | &weechat, 222 | &buffer, 223 | completion_item, 224 | &Completion::from_raw(hook_data.weechat_ptr, completion), 225 | ); 226 | 227 | if let Ok(()) = ret { 228 | WEECHAT_RC_OK 229 | } else { 230 | WEECHAT_RC_ERROR 231 | } 232 | } 233 | 234 | Weechat::check_thread(); 235 | let weechat = unsafe { Weechat::weechat() }; 236 | 237 | let data = 238 | Box::new(CompletionHookData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 239 | 240 | let data_ref = Box::leak(data); 241 | let hook_completion = weechat.get().hook_completion.unwrap(); 242 | 243 | let completion_item = LossyCString::new(completion_item); 244 | let description = LossyCString::new(description); 245 | 246 | let hook_ptr = unsafe { 247 | hook_completion( 248 | weechat.ptr, 249 | completion_item.as_ptr(), 250 | description.as_ptr(), 251 | Some(c_hook_cb), 252 | data_ref as *const _ as *const c_void, 253 | ptr::null_mut(), 254 | ) 255 | }; 256 | 257 | let hook_data = unsafe { Box::from_raw(data_ref) }; 258 | 259 | if hook_ptr.is_null() { 260 | return Err(()); 261 | } 262 | 263 | let hook = Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }; 264 | 265 | Ok(CompletionHook { _hook: hook, _hook_data: hook_data }) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/fd.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | os::{raw::c_void, unix::io::AsRawFd}, 3 | ptr, 4 | }; 5 | 6 | use libc::c_int; 7 | use weechat_sys::{t_weechat_plugin, WEECHAT_RC_OK}; 8 | 9 | use super::Hook; 10 | use crate::Weechat; 11 | 12 | /// Setting for the FdHook. 13 | pub enum FdHookMode { 14 | /// Catch read events. 15 | Read, 16 | /// Catch write events. 17 | Write, 18 | /// Catch read and write events. 19 | ReadWrite, 20 | } 21 | 22 | impl FdHookMode { 23 | pub(crate) fn as_tuple(&self) -> (i32, i32) { 24 | let read = match self { 25 | FdHookMode::Read => 1, 26 | FdHookMode::ReadWrite => 1, 27 | FdHookMode::Write => 0, 28 | }; 29 | 30 | let write = match self { 31 | FdHookMode::Read => 0, 32 | FdHookMode::ReadWrite => 1, 33 | FdHookMode::Write => 1, 34 | }; 35 | (read, write) 36 | } 37 | } 38 | 39 | /// Hook for a file descriptor, the hook is removed when the object is dropped. 40 | pub struct FdHook { 41 | _hook: Hook, 42 | _hook_data: Box>, 43 | } 44 | 45 | /// Callback trait for file descriptor based hooks. 46 | pub trait FdHookCallback { 47 | /// The concrete type of the hooked file descriptor object. 48 | type FdObject; 49 | /// The callback that will be called when data is available to be read or to 50 | /// be written on the file descriptor based object. 51 | /// 52 | /// # Arguments 53 | /// 54 | /// * `weechat` - A Weechat context. 55 | /// 56 | /// * `fd_object` - The file-descriptor based object that was registered to 57 | /// be watched for reads or writes. 58 | fn callback(&mut self, weechat: &Weechat, fd_object: &mut Self::FdObject); 59 | } 60 | 61 | struct FdHookData { 62 | callback: Box>, 63 | weechat_ptr: *mut t_weechat_plugin, 64 | fd_object: F, 65 | } 66 | 67 | impl FdHook { 68 | /// Hook an object that can be turned into a raw file descriptor. 69 | /// Returns the hook object. 70 | /// 71 | /// # Arguments 72 | /// 73 | /// * `fd_object` - An object for which the file descriptor will be watched 74 | /// and the callback called when read or write operations can happen on 75 | /// it. 76 | /// 77 | /// * `mode` - Configure the hook to watch for writes, reads or both on the 78 | /// file descriptor. 79 | /// 80 | /// * `callback` - A function that will be called if a watched event on the 81 | /// file descriptor happens. 82 | /// 83 | /// * `callback_data` - Data that will be passed to the callback every time 84 | /// the callback runs. This data will be freed when the hook is unhooked. 85 | /// 86 | /// # Panics 87 | /// 88 | /// Panics if the method is not called from the main Weechat thread. 89 | /// 90 | /// # Example 91 | /// 92 | /// ```no_run 93 | /// # use weechat::{Weechat, hooks::{FdHook, FdHookMode, FdHookCallback}}; 94 | /// # use pipe_channel::{channel, Receiver, Sender}; 95 | /// 96 | /// struct Data; 97 | /// 98 | /// impl FdHookCallback for Data { 99 | /// type FdObject = Receiver; 100 | /// 101 | /// fn callback(&mut self, _: &Weechat, receiver: &mut Receiver) { 102 | /// if let Ok(data) = receiver.recv() { 103 | /// Weechat::print(&data) 104 | /// } 105 | /// } 106 | /// } 107 | /// 108 | /// let (sender, receiver): (Sender, Receiver) = channel(); 109 | /// 110 | /// let hook = FdHook::new(receiver, FdHookMode::Read, Data) 111 | /// .expect("Can't create executor FD hook"); 112 | /// ``` 113 | pub fn new( 114 | fd_object: F, 115 | mode: FdHookMode, 116 | callback: impl FdHookCallback + 'static, 117 | ) -> Result, ()> 118 | where 119 | F: AsRawFd, 120 | { 121 | unsafe extern "C" fn c_hook_cb( 122 | pointer: *const c_void, 123 | _data: *mut c_void, 124 | _fd: i32, 125 | ) -> c_int { 126 | let hook_data: &mut FdHookData = { &mut *(pointer as *mut FdHookData) }; 127 | let cb = &mut hook_data.callback; 128 | let fd_object = &mut hook_data.fd_object; 129 | let weechat = Weechat::from_ptr(hook_data.weechat_ptr); 130 | 131 | cb.callback(&weechat, fd_object); 132 | 133 | WEECHAT_RC_OK 134 | } 135 | 136 | Weechat::check_thread(); 137 | let weechat = unsafe { Weechat::weechat() }; 138 | 139 | let fd = fd_object.as_raw_fd(); 140 | 141 | let data = Box::new(FdHookData { 142 | callback: Box::new(callback), 143 | weechat_ptr: weechat.ptr, 144 | fd_object, 145 | }); 146 | 147 | let data_ref = Box::leak(data); 148 | let hook_fd = weechat.get().hook_fd.unwrap(); 149 | let (read, write) = mode.as_tuple(); 150 | 151 | let hook_ptr = unsafe { 152 | hook_fd( 153 | weechat.ptr, 154 | fd, 155 | read, 156 | write, 157 | 0, 158 | Some(c_hook_cb::), 159 | data_ref as *const _ as *const c_void, 160 | ptr::null_mut(), 161 | ) 162 | }; 163 | 164 | let hook_data = unsafe { Box::from_raw(data_ref) }; 165 | let hook = Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }; 166 | 167 | if hook_ptr.is_null() { 168 | Err(()) 169 | } else { 170 | Ok(FdHook:: { _hook: hook, _hook_data: hook_data }) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/mod.rs: -------------------------------------------------------------------------------- 1 | //! Weechat Hook module. 2 | //! 3 | //! Weechat hooks are used for many different things, to create commands, to 4 | //! listen to events on a file descriptor, add completions to Weechat, etc. 5 | 6 | mod signal; 7 | 8 | mod bar; 9 | mod commands; 10 | mod completion; 11 | mod fd; 12 | #[cfg(feature = "unsound")] 13 | mod modifier; 14 | mod timer; 15 | 16 | pub use bar::{BarItem, BarItemCallback}; 17 | pub use commands::{Command, CommandCallback, CommandRun, CommandRunCallback, CommandSettings}; 18 | pub use completion::{Completion, CompletionCallback, CompletionHook, CompletionPosition}; 19 | pub use fd::{FdHook, FdHookCallback, FdHookMode}; 20 | #[cfg(feature = "unsound")] 21 | pub use modifier::{ModifierCallback, ModifierData, ModifierHook}; 22 | pub use signal::{SignalCallback, SignalData, SignalHook}; 23 | pub use timer::{RemainingCalls, TimerCallback, TimerHook}; 24 | use weechat_sys::{t_hook, t_weechat_plugin}; 25 | 26 | use crate::Weechat; 27 | 28 | /// Weechat Hook type. The hook is unhooked automatically when the object is 29 | /// dropped. 30 | pub(crate) struct Hook { 31 | pub(crate) ptr: *mut t_hook, 32 | pub(crate) weechat_ptr: *mut t_weechat_plugin, 33 | } 34 | 35 | impl Drop for Hook { 36 | fn drop(&mut self) { 37 | let weechat = Weechat::from_ptr(self.weechat_ptr); 38 | let unhook = weechat.get().unhook.unwrap(); 39 | unsafe { unhook(self.ptr) }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/modifier.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, os::raw::c_void, ptr}; 2 | 3 | use libc::c_char; 4 | use weechat_sys::{t_gui_buffer, t_weechat_plugin}; 5 | 6 | use super::Hook; 7 | use crate::{buffer::Buffer, LossyCString, Weechat}; 8 | 9 | /// Hook for a modifier, the hook is removed when the object is dropped. 10 | #[cfg_attr(docsrs, doc(cfg(unsound)))] 11 | pub struct ModifierHook { 12 | _hook: Hook, 13 | _hook_data: Box, 14 | } 15 | 16 | struct ModifierHookData { 17 | callback: Box, 18 | weechat_ptr: *mut t_weechat_plugin, 19 | } 20 | 21 | /// Enum over the different data types a modifier may send. 22 | pub enum ModifierData<'a> { 23 | /// String data 24 | String(Cow<'a, str>), 25 | /// Buffer that was sent with the modifier. 26 | Buffer(Buffer<'a>), 27 | } 28 | 29 | impl<'a> ModifierData<'a> { 30 | fn pointer_is_buffer(modifier_name: &str) -> bool { 31 | // This table is taken from the Weechat plugin API docs 32 | // 33 | // https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_modifier 34 | if modifier_name.starts_with("bar_condition_") { 35 | true 36 | } else { 37 | matches!( 38 | modifier_name, 39 | "bar_condition_yyy" 40 | | "history_add" 41 | | "input_text_content" 42 | | "input_text_display" 43 | | "input_text_display_with_cursor" 44 | | "input_text_for_buffer" 45 | ) 46 | } 47 | } 48 | 49 | fn from_name( 50 | weechat: &'a Weechat, 51 | modifier_name: &str, 52 | data: *const c_char, 53 | ) -> Option> { 54 | if data.is_null() { 55 | return None; 56 | } 57 | 58 | let modifier_data = unsafe { CStr::from_ptr(data).to_string_lossy() }; 59 | 60 | // Some modifier send out a buffer pointer converted to a string, 61 | // convert those to a buffer. 62 | if ModifierData::pointer_is_buffer(modifier_name) { 63 | if modifier_data.len() < 2 || !modifier_data.starts_with("0x") { 64 | None 65 | } else { 66 | let ptr = u64::from_str_radix(&modifier_data[2..], 16).ok()?; 67 | 68 | Some(ModifierData::Buffer(weechat.buffer_from_ptr(ptr as *mut t_gui_buffer))) 69 | } 70 | } else { 71 | Some(ModifierData::String(modifier_data)) 72 | } 73 | } 74 | } 75 | 76 | /// Trait for the modifier callback. 77 | /// 78 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 79 | /// be passed to the callback implement this over your struct. 80 | pub trait ModifierCallback { 81 | /// Callback that will be called when a modifier is fired. 82 | /// 83 | /// # Arguments 84 | /// 85 | /// * `weechat` - A Weechat context. 86 | /// 87 | /// * `modifier_name` - The name of the modifier that fired the callback. 88 | /// 89 | /// * `data` - The data that was passed on by the modifier. 90 | /// 91 | /// * `string` - The string that should be modified. 92 | fn callback( 93 | &mut self, 94 | weechat: &Weechat, 95 | modifier_name: &str, 96 | data: Option, 97 | string: Cow, 98 | ) -> Option; 99 | } 100 | 101 | impl, Cow) -> Option + 'static> 102 | ModifierCallback for T 103 | { 104 | /// Callback that will be called when a modifier is fired. 105 | /// 106 | /// # Arguments 107 | /// 108 | /// * `weechat` - A Weechat context. 109 | /// 110 | /// * `modifier_name` - The name of the modifier that fired the callback. 111 | /// 112 | /// * `data` - The data that was passed on by the modifier. 113 | /// 114 | /// * `string` - The string that should be modified. 115 | fn callback( 116 | &mut self, 117 | weechat: &Weechat, 118 | modifier_name: &str, 119 | data: Option, 120 | string: Cow, 121 | ) -> Option { 122 | self(weechat, modifier_name, data, string) 123 | } 124 | } 125 | 126 | impl ModifierHook { 127 | /// Hook a modifier. 128 | /// 129 | /// # Arguments 130 | /// 131 | /// * `modifier_name` - The modifier to hook. 132 | /// 133 | /// * `callback` - A function or a struct that implements ModifierCallback, 134 | /// the callback method of the trait will be called when the modifier is 135 | /// fired. 136 | /// 137 | /// # Panics 138 | /// 139 | /// Panics if the method is not called from the main Weechat thread. 140 | /// 141 | /// # Example 142 | /// 143 | /// ```no_run 144 | /// # use std::borrow::Cow; 145 | /// # use weechat::{Weechat, ReturnCode}; 146 | /// # use weechat::hooks::{ModifierData, ModifierHook}; 147 | /// 148 | /// let modifier_hook = ModifierHook::new( 149 | /// "input_text_display_with_cursor", 150 | /// |_weechat: &Weechat, 151 | /// _modifier_name: &str, 152 | /// data: Option, 153 | /// string: Cow| { 154 | /// if let ModifierData::Buffer(buffer) = data? { 155 | /// buffer.print("Modifying the input buffer") 156 | /// } 157 | /// 158 | /// None 159 | /// }); 160 | /// ``` 161 | #[cfg_attr(docsrs, doc(cfg(unsound)))] 162 | pub fn new(modifier_name: &str, callback: impl ModifierCallback + 'static) -> Result { 163 | unsafe extern "C" fn c_hook_cb( 164 | pointer: *const c_void, 165 | _data: *mut c_void, 166 | modifier_name: *const c_char, 167 | modifier_data: *const c_char, 168 | string: *const c_char, 169 | ) -> *mut c_char { 170 | let hook_data: &mut ModifierHookData = { &mut *(pointer as *mut ModifierHookData) }; 171 | let cb = &mut hook_data.callback; 172 | 173 | let modifier_name = CStr::from_ptr(modifier_name).to_str().unwrap_or_default(); 174 | 175 | let string = if string.is_null() { 176 | Cow::from("") 177 | } else { 178 | CStr::from_ptr(string).to_string_lossy() 179 | }; 180 | 181 | let weechat = Weechat::from_ptr(hook_data.weechat_ptr); 182 | 183 | let data = ModifierData::from_name(&weechat, modifier_name, modifier_data); 184 | 185 | let modified_string = cb.callback(&weechat, modifier_name, data, string); 186 | 187 | if let Some(modified_string) = modified_string { 188 | let string_length = modified_string.len(); 189 | let modified_string = LossyCString::new(modified_string); 190 | 191 | let strndup = weechat.get().strndup.unwrap(); 192 | strndup(modified_string.as_ptr(), string_length as i32) 193 | } else { 194 | ptr::null_mut() 195 | } 196 | } 197 | 198 | Weechat::check_thread(); 199 | let weechat = unsafe { Weechat::weechat() }; 200 | 201 | let data = 202 | Box::new(ModifierHookData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 203 | 204 | let data_ref = Box::leak(data); 205 | let hook_modifier = weechat.get().hook_modifier.unwrap(); 206 | 207 | let modifier_name = LossyCString::new(modifier_name); 208 | 209 | let hook_ptr = unsafe { 210 | hook_modifier( 211 | weechat.ptr, 212 | modifier_name.as_ptr(), 213 | Some(c_hook_cb), 214 | data_ref as *const _ as *const c_void, 215 | ptr::null_mut(), 216 | ) 217 | }; 218 | 219 | let hook_data = unsafe { Box::from_raw(data_ref) }; 220 | let hook = Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }; 221 | 222 | if hook_ptr.is_null() { 223 | Err(()) 224 | } else { 225 | Ok(Self { _hook: hook, _hook_data: hook_data }) 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/signal.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | cell::Cell, 4 | ffi::CStr, 5 | os::raw::{c_char, c_int, c_void}, 6 | ptr, 7 | rc::Rc, 8 | }; 9 | 10 | use weechat_sys::{t_gui_buffer, t_weechat_plugin}; 11 | 12 | use super::Hook; 13 | use crate::{ 14 | buffer::{Buffer, InnerBuffer, InnerBuffers}, 15 | LossyCString, ReturnCode, Weechat, 16 | }; 17 | 18 | /// Hook for a signal, the hook is removed when the object is dropped. 19 | pub struct SignalHook { 20 | _hook: Hook, 21 | _hook_data: Box, 22 | } 23 | 24 | struct SignalHookData { 25 | callback: Box, 26 | weechat_ptr: *mut t_weechat_plugin, 27 | } 28 | 29 | /// Enum over the different data types a signal may send. 30 | #[non_exhaustive] 31 | pub enum SignalData<'a> { 32 | /// String data 33 | String(Cow<'a, str>), 34 | /// Integer data 35 | Integer(i32), 36 | /// Buffer that was sent with the signal. 37 | Buffer(Buffer<'a>), 38 | } 39 | 40 | impl<'a> From<&'a str> for SignalData<'a> { 41 | fn from(string: &'a str) -> SignalData<'a> { 42 | SignalData::String(Cow::from(string)) 43 | } 44 | } 45 | 46 | impl From for SignalData<'_> { 47 | fn from(val: String) -> Self { 48 | SignalData::String(Cow::from(val)) 49 | } 50 | } 51 | 52 | impl From for SignalData<'_> { 53 | fn from(val: i32) -> Self { 54 | SignalData::Integer(val) 55 | } 56 | } 57 | 58 | impl<'a> From> for SignalData<'a> { 59 | fn from(val: Buffer<'a>) -> Self { 60 | SignalData::Buffer(val) 61 | } 62 | } 63 | 64 | impl<'a> From<&'a Buffer<'a>> for SignalData<'a> { 65 | fn from(val: &'a Buffer<'a>) -> Self { 66 | let ptr = val.ptr(); 67 | 68 | SignalData::Buffer(Buffer { 69 | inner: InnerBuffers::BorrowedBuffer(InnerBuffer { 70 | ptr, 71 | weechat: val.inner.weechat(), 72 | closing: Rc::new(Cell::new(false)), 73 | }), 74 | }) 75 | } 76 | } 77 | 78 | impl<'a> SignalData<'a> { 79 | fn pointer_is_buffer(signal_name: &str) -> bool { 80 | // This table is taken from the Weechat plugin API docs 81 | // 82 | // https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_signal 83 | #[allow(clippy::match_like_matches_macro)] 84 | match signal_name { 85 | "irc_channel_opened" | "irc_pv_opened" | "irc_server_opened" => true, 86 | 87 | "logger_start" | "logger_stop" | "logger_backlog" => true, 88 | 89 | "spell_suggest" => true, 90 | 91 | "buffer_opened" | "buffer_closing" | "buffer_closed" | "buffer_cleared" => true, 92 | 93 | "buffer_filters_enabled" 94 | | "buffer_filters_disabled" 95 | | "buffer_hidden" 96 | | "buffer_unhidden" => true, 97 | 98 | "buffer_lines_hidden" 99 | | "buffer_localvar_added" 100 | | "buffer_localvar_changed" 101 | | "buffer_localvar_removed" 102 | | "buffer_merged" 103 | | "buffer_unmerged" 104 | | "buffer_moved" 105 | | "buffer_renamed" 106 | | "buffer_switch" 107 | | "buffer_title_changed" 108 | | "buffer_type_changed" => true, 109 | 110 | "buffer_zoomed" | "buffer_unzoomed" => true, 111 | 112 | "hotlist_changed" => true, 113 | 114 | "input_search" | "input_text_changed" | "input_text_cursor_moved" => true, 115 | 116 | // TODO nicklist group signals have a string representation of a 117 | // pointer concatenated to the group name 118 | 119 | // TODO some signals send out pointers to windows. 120 | // TODO some signals send out pointers to infolists. 121 | _ => false, 122 | } 123 | } 124 | 125 | fn from_type_and_name( 126 | weechat: &'a Weechat, 127 | signal_name: &str, 128 | data_type: &str, 129 | data: *mut c_void, 130 | ) -> Option> { 131 | // Some signals don't send any data, some other signals might send out a 132 | // buffer pointer that might be null, in either case check if the 133 | // pointer is valid first. 134 | if data.is_null() { 135 | return None; 136 | } 137 | 138 | match data_type { 139 | "string" => unsafe { 140 | Some(SignalData::String(CStr::from_ptr(data as *const c_char).to_string_lossy())) 141 | }, 142 | "integer" => { 143 | let data = data as *const c_int; 144 | unsafe { Some(SignalData::Integer(*(data))) } 145 | } 146 | "pointer" => { 147 | if SignalData::pointer_is_buffer(signal_name) { 148 | Some(SignalData::Buffer(weechat.buffer_from_ptr(data as *mut t_gui_buffer))) 149 | } else { 150 | None 151 | } 152 | } 153 | _ => None, 154 | } 155 | } 156 | } 157 | 158 | /// Trait for the signal callback. 159 | /// 160 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 161 | /// be passed to the callback implement this over your struct. 162 | pub trait SignalCallback { 163 | /// Callback that will be called when a signal is fired. 164 | /// input field. 165 | /// 166 | /// # Arguments 167 | /// 168 | /// * `weechat` - A Weechat context. 169 | /// 170 | /// * `signal_name` - The name of the signal that fired the callback. 171 | /// 172 | /// * `data` - The data that was passed on by the signal. 173 | fn callback( 174 | &mut self, 175 | weechat: &Weechat, 176 | signal_name: &str, 177 | data: Option, 178 | ) -> ReturnCode; 179 | } 180 | 181 | impl) -> ReturnCode + 'static> SignalCallback for T { 182 | fn callback( 183 | &mut self, 184 | weechat: &Weechat, 185 | signal_name: &str, 186 | data: Option, 187 | ) -> ReturnCode { 188 | self(weechat, signal_name, data) 189 | } 190 | } 191 | 192 | impl SignalHook { 193 | /// Hook a signal. 194 | /// 195 | /// # Arguments 196 | /// 197 | /// * `signal_name` - The signal to hook (wildcard `*` is allowed). 198 | /// 199 | /// * `callback` - A function or a struct that implements SignalCallback, 200 | /// the callback method of the trait will be called when the signal is 201 | /// fired. 202 | /// 203 | /// # Panics 204 | /// 205 | /// Panics if the method is not called from the main Weechat thread. 206 | /// 207 | /// # Example 208 | /// 209 | /// ```no_run 210 | /// # use weechat::{Weechat, ReturnCode}; 211 | /// # use weechat::hooks::{SignalData, SignalHook}; 212 | /// let signal_hook = SignalHook::new( 213 | /// "buffer_switch", 214 | /// |_weechat: &Weechat, _signal_name: &str, data: Option| { 215 | /// if let Some(data) = data { 216 | /// match data { 217 | /// SignalData::Buffer(buffer) => { 218 | /// buffer.print("Switched buffer") 219 | /// } 220 | /// _ => (), 221 | /// } 222 | /// } 223 | /// 224 | /// ReturnCode::Ok 225 | /// }, 226 | /// ); 227 | /// ``` 228 | pub fn new(signal_name: &str, callback: impl SignalCallback + 'static) -> Result { 229 | unsafe extern "C" fn c_hook_cb( 230 | pointer: *const c_void, 231 | _data: *mut c_void, 232 | signal_name: *const c_char, 233 | data_type: *const c_char, 234 | signal_data: *mut c_void, 235 | ) -> c_int { 236 | let hook_data: &mut SignalHookData = { &mut *(pointer as *mut SignalHookData) }; 237 | let cb = &mut hook_data.callback; 238 | 239 | let data_type = CStr::from_ptr(data_type).to_str().unwrap_or_default(); 240 | let signal_name = CStr::from_ptr(signal_name).to_str().unwrap_or_default(); 241 | 242 | let weechat = Weechat::from_ptr(hook_data.weechat_ptr); 243 | let data = 244 | SignalData::from_type_and_name(&weechat, signal_name, data_type, signal_data); 245 | 246 | cb.callback(&weechat, signal_name, data) as i32 247 | } 248 | 249 | Weechat::check_thread(); 250 | let weechat = unsafe { Weechat::weechat() }; 251 | 252 | let data = 253 | Box::new(SignalHookData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 254 | 255 | let data_ref = Box::leak(data); 256 | let hook_signal = weechat.get().hook_signal.unwrap(); 257 | 258 | let signal_name = LossyCString::new(signal_name); 259 | 260 | let hook_ptr = unsafe { 261 | hook_signal( 262 | weechat.ptr, 263 | signal_name.as_ptr(), 264 | Some(c_hook_cb), 265 | data_ref as *const _ as *const c_void, 266 | ptr::null_mut(), 267 | ) 268 | }; 269 | 270 | let hook_data = unsafe { Box::from_raw(data_ref) }; 271 | let hook = Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }; 272 | 273 | if hook_ptr.is_null() { 274 | Err(()) 275 | } else { 276 | Ok(SignalHook { _hook: hook, _hook_data: hook_data }) 277 | } 278 | } 279 | } 280 | 281 | impl Weechat { 282 | /// Send a signal. 283 | /// 284 | /// This will send out a signal and callbacks that are registered with a 285 | /// `SignalHook` to listen to that signal will get called. 286 | /// 287 | /// # Arguments 288 | /// 289 | /// * `signal_name` - The name of the signal that should be sent out. Common 290 | /// signals can be found in the Weechat plugin API [reference]. 291 | /// 292 | /// * `data` - Data that should be provided to the signal callback. This can 293 | /// be a string, an i32 number, or a buffer. 294 | /// 295 | /// ```no_run 296 | /// # use weechat::Weechat; 297 | /// # use weechat::buffer::BufferBuilder; 298 | /// # let buffer_handle = BufferBuilder::new("test") 299 | /// # .build() 300 | /// # .unwrap(); 301 | /// # let buffer = buffer_handle.upgrade().unwrap(); 302 | /// // Fetch the chat history for the buffer. 303 | /// Weechat::hook_signal_send("logger_backlog", &buffer); 304 | /// 305 | /// // Signal that the input text changed. 306 | /// Weechat::hook_signal_send("input_text_changed", ""); 307 | /// ``` 308 | /// 309 | /// [reference]: https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_signal_send 310 | pub fn hook_signal_send<'a, D: Into>>(signal_name: &str, data: D) -> ReturnCode { 311 | Weechat::check_thread(); 312 | let weechat = unsafe { Weechat::weechat() }; 313 | 314 | let signal_name = LossyCString::new(signal_name); 315 | let signal_send = weechat.get().hook_signal_send.unwrap(); 316 | let data = data.into(); 317 | 318 | let ret = if let SignalData::String(string) = data { 319 | let string = LossyCString::new(string); 320 | unsafe { 321 | signal_send( 322 | signal_name.as_ptr(), 323 | weechat_sys::WEECHAT_HOOK_SIGNAL_STRING as *const _ as *const c_char, 324 | string.as_ptr() as *mut _, 325 | ) 326 | } 327 | } else { 328 | let (ptr, data_type) = match data { 329 | SignalData::Integer(number) => { 330 | (number as *mut _, weechat_sys::WEECHAT_HOOK_SIGNAL_INT as *const u8) 331 | } 332 | SignalData::Buffer(buffer) => { 333 | (buffer.ptr() as *mut _, weechat_sys::WEECHAT_HOOK_SIGNAL_POINTER as *const u8) 334 | } 335 | SignalData::String(_) => unreachable!(), 336 | }; 337 | unsafe { signal_send(signal_name.as_ptr(), data_type as *const c_char, ptr) } 338 | }; 339 | 340 | match ret { 341 | weechat_sys::WEECHAT_RC_OK => ReturnCode::Ok, 342 | weechat_sys::WEECHAT_RC_OK_EAT => ReturnCode::OkEat, 343 | weechat_sys::WEECHAT_RC_ERROR => ReturnCode::Error, 344 | _ => ReturnCode::Error, 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /crates/weechat/src/hooks/timer.rs: -------------------------------------------------------------------------------- 1 | use std::{os::raw::c_void, ptr, time::Duration}; 2 | 3 | use libc::c_int; 4 | use weechat_sys::{t_weechat_plugin, WEECHAT_RC_OK}; 5 | 6 | use super::Hook; 7 | use crate::Weechat; 8 | 9 | /// A hook for a timer, the hook will be removed when the object is dropped. 10 | pub struct TimerHook { 11 | _hook: Hook, 12 | _hook_data: Box, 13 | } 14 | 15 | /// Enum representing how many calls a timer still has. 16 | pub enum RemainingCalls { 17 | /// Infinitely many remaining calls. 18 | Infinite, 19 | /// A finite number of calls is remaining. 20 | Finite(i32), 21 | } 22 | 23 | impl From for RemainingCalls { 24 | fn from(remaining: i32) -> Self { 25 | match remaining { 26 | -1 => RemainingCalls::Infinite, 27 | r => RemainingCalls::Finite(r), 28 | } 29 | } 30 | } 31 | 32 | /// Trait for the timer callback 33 | /// 34 | /// A blanket implementation for pure `FnMut` functions exists, if data needs to 35 | /// be passed to the callback implement this over your struct. 36 | pub trait TimerCallback { 37 | /// Callback that will be called when the timer fires. 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `weechat` - A Weechat context. 42 | /// 43 | /// * `remaining_calls` - How many times the timer will fire. 44 | fn callback(&mut self, weechat: &Weechat, remaining_calls: RemainingCalls); 45 | } 46 | 47 | impl TimerCallback for T { 48 | fn callback(&mut self, weechat: &Weechat, remaining_calls: RemainingCalls) { 49 | self(weechat, remaining_calls) 50 | } 51 | } 52 | 53 | struct TimerHookData { 54 | callback: Box, 55 | weechat_ptr: *mut t_weechat_plugin, 56 | } 57 | 58 | impl TimerHook { 59 | /// Create a timer that will repeatedly fire. 60 | /// 61 | /// # Arguments 62 | /// 63 | /// * `interval` - The delay between calls in milliseconds. 64 | /// 65 | /// * `align_second` - The alignment on a second. For example, if the 66 | /// current time is 09:00, if the interval = 60000 (60 seconds), and 67 | /// align_second = 60, then timer is called each minute on the 0th second. 68 | /// 69 | /// * `max_calls` - The number of times the callback should be called, 0 70 | /// means it's called forever. 71 | /// 72 | /// * `callback` - A function that will be called when the timer fires, the 73 | /// `remaining` argument will be -1 if the timer has no end. 74 | /// 75 | /// # Panics 76 | /// 77 | /// Panics if the method is not called from the main Weechat thread. 78 | /// 79 | /// # Example 80 | /// ```no_run 81 | /// # use std::time::Duration; 82 | /// # use weechat::{Weechat}; 83 | /// # use weechat::hooks::{TimerHook, RemainingCalls}; 84 | /// 85 | /// let timer = TimerHook::new( 86 | /// Duration::from_secs(1), 0, -1, 87 | /// |_: &Weechat, _: RemainingCalls| { 88 | /// Weechat::print("Running timer hook"); 89 | /// } 90 | /// ).expect("Can't create timer hook"); 91 | /// ``` 92 | pub fn new( 93 | interval: Duration, 94 | align_second: i32, 95 | max_calls: i32, 96 | callback: impl TimerCallback + 'static, 97 | ) -> Result { 98 | unsafe extern "C" fn c_hook_cb( 99 | pointer: *const c_void, 100 | _data: *mut c_void, 101 | remaining: i32, 102 | ) -> c_int { 103 | let hook_data: &mut TimerHookData = { &mut *(pointer as *mut TimerHookData) }; 104 | let cb = &mut hook_data.callback; 105 | 106 | cb.callback(&Weechat::from_ptr(hook_data.weechat_ptr), RemainingCalls::from(remaining)); 107 | 108 | WEECHAT_RC_OK 109 | } 110 | 111 | Weechat::check_thread(); 112 | let weechat = unsafe { Weechat::weechat() }; 113 | 114 | let data = 115 | Box::new(TimerHookData { callback: Box::new(callback), weechat_ptr: weechat.ptr }); 116 | 117 | let data_ref = Box::leak(data); 118 | let hook_timer = weechat.get().hook_timer.unwrap(); 119 | 120 | let hook_ptr = unsafe { 121 | hook_timer( 122 | weechat.ptr, 123 | interval.as_millis() as _, 124 | align_second, 125 | max_calls, 126 | Some(c_hook_cb), 127 | data_ref as *const _ as *const c_void, 128 | ptr::null_mut(), 129 | ) 130 | }; 131 | let hook_data = unsafe { Box::from_raw(data_ref) }; 132 | 133 | if hook_ptr.is_null() { 134 | Err(()) 135 | } else { 136 | Ok(TimerHook { 137 | _hook: Hook { ptr: hook_ptr, weechat_ptr: weechat.ptr }, 138 | _hook_data: hook_data, 139 | }) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /crates/weechat/src/infolist.rs: -------------------------------------------------------------------------------- 1 | //! Infolists can be used to share information between scripts and plugins. 2 | //! 3 | //! The list of available infolists can be found in the Weechat plugin API 4 | //! reference. 5 | //! 6 | //! # Example 7 | //! 8 | //! # Examples 9 | //! ```no_run 10 | //! # use weechat::Weechat; 11 | //! # let weechat = unsafe { weechat::Weechat::weechat() }; 12 | //! let infolist = weechat.get_infolist("hook", Some("infolist")).unwrap(); 13 | //! 14 | //! for item in infolist { 15 | //! for variable in &item { 16 | //! Weechat::print(&format!("{:?}", variable)); 17 | //! } 18 | //! } 19 | //! ``` 20 | 21 | use std::{ 22 | borrow::Cow, 23 | cell::Cell, 24 | collections::{ 25 | hash_map::{IntoIter as IterHashmap, Keys}, 26 | HashMap, 27 | }, 28 | ffi::CStr, 29 | fmt::Debug, 30 | marker::PhantomData, 31 | ptr, 32 | rc::Rc, 33 | time::{Duration, SystemTime}, 34 | }; 35 | 36 | use weechat_sys::{t_gui_buffer, t_infolist}; 37 | 38 | use crate::{ 39 | buffer::{Buffer, InnerBuffer, InnerBuffers}, 40 | LossyCString, Weechat, 41 | }; 42 | 43 | /// An infolist is a list of items. 44 | /// 45 | /// Each item contains one or more variables. 46 | pub struct Infolist<'a> { 47 | ptr: *mut t_infolist, 48 | infolist_name: String, 49 | weechat: &'a Weechat, 50 | } 51 | 52 | /// The type of an infolist variable. 53 | #[derive(Eq, Hash, Debug, PartialEq, Clone)] 54 | #[allow(missing_docs)] 55 | pub enum InfolistType { 56 | Integer, 57 | String, 58 | Time, 59 | Buffer, 60 | } 61 | 62 | impl From<&str> for InfolistType { 63 | fn from(value: &str) -> Self { 64 | match value { 65 | "i" => InfolistType::Integer, 66 | "s" => InfolistType::String, 67 | "t" => InfolistType::Time, 68 | "p" => InfolistType::Buffer, 69 | v => panic!("Got unexpected value {}", v), 70 | } 71 | } 72 | } 73 | 74 | /// An item of the infolist. 75 | /// 76 | /// Each infolist item may contain multiple values. It essentially acts as a 77 | /// hashmap. 78 | pub struct InfolistItem<'a> { 79 | ptr: *mut t_infolist, 80 | weechat: Weechat, 81 | fields: HashMap, 82 | infolist: PhantomData<&'a Infolist<'a>>, 83 | } 84 | 85 | impl Debug for InfolistItem<'_> { 86 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 87 | f.debug_map().entries(self.fields.iter()).finish() 88 | } 89 | } 90 | 91 | impl<'a> InfolistItem<'a> { 92 | fn integer(&self, name: &str) -> i32 { 93 | let name = LossyCString::new(name); 94 | 95 | let infolist_integer = self.weechat.get().infolist_integer.unwrap(); 96 | 97 | unsafe { infolist_integer(self.ptr, name.as_ptr()) } 98 | } 99 | 100 | fn string(&self, name: &str) -> Option> { 101 | let name = LossyCString::new(name); 102 | 103 | let infolist_string = self.weechat.get().infolist_string.unwrap(); 104 | 105 | unsafe { 106 | let ptr = infolist_string(self.ptr, name.as_ptr()); 107 | if ptr.is_null() { 108 | None 109 | } else { 110 | Some(CStr::from_ptr(ptr).to_string_lossy()) 111 | } 112 | } 113 | } 114 | 115 | fn buffer(&self, name: &str) -> Option { 116 | let name = LossyCString::new(name); 117 | 118 | let infolist_pointer = self.weechat.get().infolist_pointer.unwrap(); 119 | 120 | let ptr = unsafe { infolist_pointer(self.ptr, name.as_ptr()) as *mut t_gui_buffer }; 121 | 122 | if ptr.is_null() { 123 | return None; 124 | } 125 | 126 | Some(Buffer { 127 | inner: InnerBuffers::BorrowedBuffer(InnerBuffer { 128 | weechat: &self.weechat, 129 | ptr, 130 | closing: Rc::new(Cell::new(false)), 131 | }), 132 | }) 133 | } 134 | 135 | fn time(&self, name: &str) -> Option { 136 | let name = LossyCString::new(name); 137 | 138 | let infolist_time = self.weechat.get().infolist_time.unwrap(); 139 | 140 | let time = unsafe { infolist_time(self.ptr, name.as_ptr()) }; 141 | 142 | let unix = SystemTime::UNIX_EPOCH; 143 | let duration = Duration::from_secs(time as u64); 144 | 145 | unix.checked_add(duration) 146 | } 147 | 148 | /// Get a variable from the current infolist item. 149 | /// 150 | /// # Arguments 151 | /// 152 | /// * `key` - The name of the variable that should be fetched. 153 | pub fn get(&self, key: &str) -> Option { 154 | let infolist_type = self.fields.get(key)?; 155 | 156 | let variable = match infolist_type { 157 | InfolistType::Integer => InfolistVariable::Integer(self.integer(key)), 158 | InfolistType::String => InfolistVariable::String(self.string(key)?), 159 | InfolistType::Time => InfolistVariable::Time(self.time(key)?), 160 | InfolistType::Buffer => InfolistVariable::Buffer(self.buffer(key)?), 161 | }; 162 | 163 | Some(variable) 164 | } 165 | 166 | /// Get the list of infolist variables that this item has. 167 | pub fn keys(&self) -> Keys<'_, String, InfolistType> { 168 | self.fields.keys() 169 | } 170 | 171 | /// An iterator visiting all variables in an infolist item. 172 | /// The iterator element type a tuple of a string containing the variable 173 | /// name and the variable itself. 174 | /// 175 | /// # Examples 176 | /// ```no_run 177 | /// # use weechat::Weechat; 178 | /// # use weechat::infolist::InfolistVariable; 179 | /// # let weechat = unsafe { weechat::Weechat::weechat() }; 180 | /// let infolist = weechat.get_infolist("buffer", None).unwrap(); 181 | /// 182 | /// for item in infolist { 183 | /// for variable in &item { 184 | /// Weechat::print(&format!("{:?}", variable)); 185 | /// } 186 | /// } 187 | /// ``` 188 | pub fn iter(&'a self) -> Iter<'a> { 189 | Iter { keys: self.fields.clone().into_iter(), item: self } 190 | } 191 | } 192 | 193 | /// An iterator over the entries of a `InfolistItem`. 194 | /// 195 | /// This `struct` is created by the [`iter`] method on [`InfolistItem`]. See its 196 | /// documentation for more. 197 | /// 198 | /// [`iter`]: struct.InfolistItem.html#method.iter 199 | /// [`InfolistItem`]: struct.InfolistItem.html 200 | pub struct Iter<'a> { 201 | item: &'a InfolistItem<'a>, 202 | keys: IterHashmap, 203 | } 204 | 205 | impl<'a> Iterator for Iter<'a> { 206 | type Item = (String, InfolistVariable<'a>); 207 | 208 | fn next(&mut self) -> Option { 209 | loop { 210 | let (name, _) = self.keys.next()?; 211 | let variable = self.item.get(&name); 212 | 213 | if let Some(variable) = variable { 214 | return Some((name, variable)); 215 | } 216 | } 217 | } 218 | } 219 | 220 | impl<'a> IntoIterator for &'a InfolistItem<'a> { 221 | type Item = (String, InfolistVariable<'a>); 222 | type IntoIter = Iter<'a>; 223 | 224 | fn into_iter(self) -> Self::IntoIter { 225 | self.iter() 226 | } 227 | } 228 | 229 | /// A variable that was fetched out of the infolist item. 230 | #[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] 231 | pub enum InfolistVariable<'a> { 232 | /// Represents an infolist integer variable. 233 | Integer(i32), 234 | /// Represents an infolist string variable. 235 | String(Cow<'a, str>), 236 | /// Represents an infolist time-based variable. 237 | Time(SystemTime), 238 | /// Represents an infolist GUI buffer variable. 239 | Buffer(Buffer<'a>), 240 | } 241 | 242 | impl Infolist<'_> { 243 | fn is_pointer_buffer(infolist_name: &str, variable_name: &str) -> bool { 244 | matches!( 245 | (infolist_name, variable_name), 246 | ("logger_buffer", "buffer") 247 | | ("buffer", "pointer") 248 | | ("buflist", "buffer") 249 | | ("irc_server", "buffer") 250 | | ("hotlist", "buffer_pointer") 251 | | ("window", "buffer") 252 | ) 253 | } 254 | 255 | fn get_fields(&self) -> HashMap { 256 | let infolist_fields = self.weechat.get().infolist_fields.unwrap(); 257 | let mut fields: HashMap = HashMap::new(); 258 | 259 | let fields_string = unsafe { 260 | let ptr = infolist_fields(self.ptr); 261 | CStr::from_ptr(ptr).to_string_lossy() 262 | }; 263 | 264 | for field in fields_string.split(',') { 265 | let split: Vec<&str> = field.split(':').collect(); 266 | 267 | let infolist_type = split[0]; 268 | let name = split[1]; 269 | 270 | // Skip the buffer, we can't safely expose them 271 | // without knowing the size of the buffer. (Note the buffer here 272 | // isn't a GUI buffer but a vector like thing. 273 | if infolist_type == "b" { 274 | continue; 275 | } 276 | 277 | let field = if infolist_type == "p" { 278 | if Infolist::is_pointer_buffer(&self.infolist_name, name) { 279 | InfolistType::Buffer 280 | } else { 281 | continue; 282 | } 283 | } else { 284 | InfolistType::from(infolist_type) 285 | }; 286 | 287 | fields.insert(name.to_owned(), field); 288 | } 289 | 290 | fields 291 | } 292 | } 293 | 294 | impl Drop for Infolist<'_> { 295 | fn drop(&mut self) { 296 | let infolist_free = self.weechat.get().infolist_free.unwrap(); 297 | 298 | unsafe { infolist_free(self.ptr) } 299 | } 300 | } 301 | 302 | impl Weechat { 303 | /// Get the infolist with the given name. 304 | /// 305 | /// # Arguments 306 | /// 307 | /// * `infolist_name` - The name of the infolist to fetch, valid values for 308 | /// this can be found in the Weechat documentation. 309 | /// 310 | /// * `arguments` - Arguments that should be passed to Weechat while 311 | /// fetching the infolist, the format of this will depend on the infolist 312 | /// that is being fetched. A list of infolists and their accompanying 313 | /// arguments can be found in the Weechat documentation. 314 | /// 315 | /// # Example 316 | /// 317 | /// ```no_run 318 | /// # use weechat::infolist::InfolistVariable; 319 | /// # let weechat = unsafe { weechat::Weechat::weechat() }; 320 | /// let infolist = weechat.get_infolist("logger_buffer", None).unwrap(); 321 | /// 322 | /// for item in infolist { 323 | /// let info_buffer = if let Some(buffer) = item.get("buffer") { 324 | /// buffer 325 | /// } else { 326 | /// continue; 327 | /// }; 328 | /// 329 | /// if let InfolistVariable::Buffer(info_buffer) = info_buffer { 330 | /// info_buffer.print("Hello world"); 331 | /// } 332 | /// } 333 | /// ``` 334 | pub fn get_infolist( 335 | &self, 336 | infolist_name: &str, 337 | arguments: Option<&str>, 338 | ) -> Result { 339 | let infolist_get = self.get().infolist_get.unwrap(); 340 | 341 | let name = LossyCString::new(infolist_name); 342 | let arguments = arguments.map(LossyCString::new); 343 | 344 | let infolist_ptr = unsafe { 345 | infolist_get( 346 | self.ptr, 347 | name.as_ptr(), 348 | ptr::null_mut(), 349 | arguments.map_or(ptr::null_mut(), |a| a.as_ptr()), 350 | ) 351 | }; 352 | 353 | if infolist_ptr.is_null() { 354 | Err(()) 355 | } else { 356 | Ok(Infolist { 357 | ptr: infolist_ptr, 358 | infolist_name: infolist_name.to_owned(), 359 | weechat: self, 360 | }) 361 | } 362 | } 363 | } 364 | 365 | impl<'a> Iterator for Infolist<'a> { 366 | type Item = InfolistItem<'a>; 367 | 368 | fn next(&mut self) -> Option> { 369 | let infolist_next = self.weechat.get().infolist_next.unwrap(); 370 | 371 | let ret = unsafe { infolist_next(self.ptr) }; 372 | 373 | if ret == 1 { 374 | let fields = self.get_fields(); 375 | 376 | Some(InfolistItem { 377 | ptr: self.ptr, 378 | weechat: Weechat::from_ptr(self.weechat.ptr), 379 | fields, 380 | infolist: PhantomData, 381 | }) 382 | } else { 383 | None 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /crates/weechat/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # `weechat` 2 | //! 3 | //! This crate implements high level bindings for the Weechat plugin API. 4 | //! 5 | //! The bindings make it possible to create powerful Weechat plugins using Rust. 6 | //! 7 | //! ```no_run 8 | //! use weechat::{ 9 | //! buffer::Buffer, 10 | //! plugin, Args, Weechat, Plugin, 11 | //! }; 12 | //! 13 | //! struct HelloWorld; 14 | //! 15 | //! impl Plugin for HelloWorld { 16 | //! fn init(_: &Weechat, _: Args) -> Result { 17 | //! Weechat::print("Hello from Rust"); 18 | //! Ok(Self) 19 | //! } 20 | //! } 21 | //! 22 | //! impl Drop for HelloWorld { 23 | //! fn drop(&mut self) { 24 | //! Weechat::print("Bye from Rust"); 25 | //! } 26 | //! } 27 | //! 28 | //! plugin!( 29 | //! HelloWorld, 30 | //! name: "hello", 31 | //! author: "Damir Jelić ", 32 | //! description: "Simple hello world Rust plugin", 33 | //! version: "1.0.0", 34 | //! license: "MIT" 35 | //! ); 36 | //! ``` 37 | 38 | #![deny(missing_docs)] 39 | #![allow(clippy::result_unit_err)] 40 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 41 | 42 | use std::ffi::CString; 43 | 44 | #[cfg(feature = "async")] 45 | mod executor; 46 | mod hashtable; 47 | mod hdata; 48 | mod weechat; 49 | 50 | #[cfg(feature = "config_macro")] 51 | #[macro_use] 52 | mod config_macros; 53 | 54 | #[cfg(feature = "config_macro")] 55 | pub use paste; 56 | #[cfg(feature = "config_macro")] 57 | pub use strum; 58 | 59 | pub mod buffer; 60 | pub mod config; 61 | pub mod hooks; 62 | pub mod infolist; 63 | 64 | pub use libc; 65 | pub use weechat_macro::plugin; 66 | pub use weechat_sys; 67 | 68 | pub use crate::weechat::{Args, Prefix, Weechat}; 69 | 70 | /// Weechat plugin trait. 71 | /// 72 | /// Implement this trait over your struct to implement a Weechat plugin. The 73 | /// init method will get called when Weechat loads the plugin, while the 74 | /// 75 | /// Drop method will be called when Weechat unloads the plugin. 76 | pub trait Plugin: Sized { 77 | /// The initialization method for the plugin. 78 | /// 79 | /// This will be called when Weechat loads the plugin. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `weechat` - A borrow to a Weechat object that will be valid during the 84 | /// duration of the init callback. 85 | /// 86 | /// * `args` - Arguments passed to the plugin when it is loaded. 87 | fn init(weechat: &Weechat, args: Args) -> Result; 88 | } 89 | 90 | #[cfg(feature = "async")] 91 | pub use executor::Task; 92 | 93 | /// Status values for Weechat callbacks 94 | pub enum ReturnCode { 95 | /// The callback returned successfully. 96 | Ok = weechat_sys::WEECHAT_RC_OK as isize, 97 | /// The callback returned successfully and the command will not be executed 98 | /// after the callback. 99 | OkEat = weechat_sys::WEECHAT_RC_OK_EAT as isize, 100 | /// The callback returned with an error. 101 | Error = weechat_sys::WEECHAT_RC_ERROR as isize, 102 | } 103 | 104 | pub(crate) struct LossyCString; 105 | 106 | impl LossyCString { 107 | #[allow(clippy::new_ret_no_self)] 108 | pub(crate) fn new>(t: T) -> CString { 109 | match CString::new(t.as_ref()) { 110 | Ok(cstr) => cstr, 111 | Err(_) => CString::new(t.as_ref().replace('\0', "")).expect("string has no nulls"), 112 | } 113 | } 114 | } 115 | --------------------------------------------------------------------------------