feat(teipe): initial commit
This commit is contained in:
3
libxkbcommonrs/.clangd
Normal file
3
libxkbcommonrs/.clangd
Normal file
@@ -0,0 +1,3 @@
|
||||
CompileFlags:
|
||||
Add: [-xc, -Wall, -Werror, -Wpedantic, -std=c17]
|
||||
Compiler: clang
|
||||
15
libxkbcommonrs/Cargo.toml
Normal file
15
libxkbcommonrs/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "libxkbcommonrs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = { workspace = true, features = ["experimental"] }
|
||||
anyhow.workspace = true
|
||||
cc = "1.1"
|
||||
convert_case = "0.6"
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["rt", "net"] }
|
||||
tokio-stream = { workspace = true }
|
||||
log = { workspace = true, features = ["kv"] }
|
||||
209
libxkbcommonrs/build.rs
Normal file
209
libxkbcommonrs/build.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use bindgen::callbacks::{EnumVariantValue, ParseCallbacks};
|
||||
use convert_case::{Case, Casing};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RemovePrefixesCallback {}
|
||||
|
||||
impl RemovePrefixesCallback {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn boxed() -> Box<dyn ParseCallbacks> {
|
||||
Box::new(Self::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseCallbacks for RemovePrefixesCallback {
|
||||
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
||||
const PREFIXES: &[&str] = &["XKB_", "xkb_"];
|
||||
let mut ret = None;
|
||||
for prefix in PREFIXES {
|
||||
if let Some(name) = original_item_name.strip_prefix(prefix) {
|
||||
ret = Some(name.to_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn enum_variant_name(
|
||||
&self,
|
||||
enum_name: Option<&str>,
|
||||
original_variant_name: &str,
|
||||
_variant_value: EnumVariantValue,
|
||||
) -> Option<String> {
|
||||
if enum_name.is_none() {
|
||||
return self.item_name(original_variant_name);
|
||||
}
|
||||
// let original_variant_name = self
|
||||
// .item_name(original_variant_name)
|
||||
// .unwrap_or(original_variant_name.to_owned());
|
||||
let enum_name = enum_name.unwrap();
|
||||
let enum_name = enum_name.strip_prefix("enum ").unwrap_or(enum_name);
|
||||
let enum_lower = original_variant_name.to_lowercase();
|
||||
let diff_index = enum_lower
|
||||
.chars()
|
||||
.zip(enum_name.chars())
|
||||
.enumerate()
|
||||
.find(|(_i, (v, e))| v != e)
|
||||
.map(|it| it.0)
|
||||
.unwrap_or(enum_name.len());
|
||||
let name = original_variant_name[diff_index..]
|
||||
.trim_start_matches('_')
|
||||
.to_case(Case::Pascal);
|
||||
|
||||
Some(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TypedefCallback {}
|
||||
|
||||
impl TypedefCallback {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn boxed() -> Box<dyn ParseCallbacks> {
|
||||
Box::new(Self::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseCallbacks for TypedefCallback {
|
||||
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
||||
if original_item_name == "xkb_keysym_t" {
|
||||
Some(String::from("Keysym"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn int_macro(&self, name: &str, _value: i64) -> Option<IntKind> {
|
||||
// if name.starts_with("XKB_KEY_") {
|
||||
// Some(IntKind::Custom {
|
||||
// name: "Keysym",
|
||||
// is_signed: false,
|
||||
// })
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec<String> {
|
||||
// if (info.name.contains("keysym") && !info.name.contains("flags"))
|
||||
// || info.name == "Keysym"
|
||||
// || info.name == "xkb_keysym_t"
|
||||
// || info.name == "keysym_t"
|
||||
// {
|
||||
// panic!("{info:?}");
|
||||
// vec![
|
||||
// "PartialEq".to_string(),
|
||||
// "PartialOrd".to_string(),
|
||||
// "Ord".to_string(),
|
||||
// "Eq".to_string(),
|
||||
// ]
|
||||
// } else {
|
||||
// vec![]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// #[derive(Debug)]
|
||||
// struct VariadicsCallback {}
|
||||
//
|
||||
// impl VariadicsCallback {
|
||||
// fn new() -> Self {
|
||||
// Self {}
|
||||
// }
|
||||
// fn boxed() -> Box<dyn ParseCallbacks> {
|
||||
// Box::new(Self::new())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl ParseCallbacks for VariadicsCallback {
|
||||
// fn wrap_as_variadic_fn(&self, name: &str) -> Option<String> {
|
||||
// panic!("mdr {name}");
|
||||
// Some(format!("{name}_var"))
|
||||
// }
|
||||
// }
|
||||
|
||||
fn build_xkb_keysyms() -> anyhow::Result<()> {
|
||||
let common_bindings = bindgen::builder()
|
||||
.header("/usr/include/xkbcommon/xkbcommon-keysyms.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.parse_callbacks(RemovePrefixesCallback::boxed())
|
||||
.parse_callbacks(TypedefCallback::boxed())
|
||||
.allowlist_var("XKB_.*")
|
||||
.generate()?;
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let out_file = out_path.join("libxkbcommon-keysyms.rs");
|
||||
common_bindings
|
||||
.write_to_file(out_file)
|
||||
.expect("Couldn't write bindings!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_xkbcommon() -> anyhow::Result<()> {
|
||||
let common_bindings = bindgen::builder()
|
||||
.header("ffi/logger.h")
|
||||
.header("/usr/include/xkbcommon/xkbcommon.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.parse_callbacks(RemovePrefixesCallback::boxed())
|
||||
.parse_callbacks(TypedefCallback::boxed())
|
||||
// .parse_callbacks(VariadicsCallback::boxed())
|
||||
.allowlist_item("xkb_.*")
|
||||
.allowlist_item("log_.*") // Log wrapper
|
||||
// .allowlist_item("XKB_.*")
|
||||
.new_type_alias_deref("Keysym")
|
||||
.no_debug("xkb_keysym_t")
|
||||
.allowlist_var("XKB_KEYCODE_INVALID")
|
||||
.allowlist_var("XKB_LAYOUT_INVALID")
|
||||
.allowlist_var("XKB_LEVEL_INVALID")
|
||||
.allowlist_var("XKB_MOD_INVALID")
|
||||
.allowlist_var("XKB_LED_INVALID")
|
||||
.allowlist_var("XKB_KEYCODE_MAX")
|
||||
.allowlist_var("XKB_KEYSYM_MAX")
|
||||
.default_enum_style(bindgen::EnumVariation::NewType {
|
||||
is_bitfield: false,
|
||||
is_global: false,
|
||||
})
|
||||
.derive_eq(true)
|
||||
.derive_partialeq(true)
|
||||
.derive_ord(true)
|
||||
.derive_partialord(true)
|
||||
.derive_hash(true)
|
||||
.generate()?;
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let out_file = out_path.join("libxkbcommon.rs");
|
||||
common_bindings
|
||||
.write_to_file(out_file)
|
||||
.expect("Couldn't write bindings!");
|
||||
|
||||
println!("cargo:rustc-link-lib=xkbcommon");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_logger() -> anyhow::Result<()> {
|
||||
cc::Build::new()
|
||||
.file("ffi/logger.c")
|
||||
.std("c17")
|
||||
.warnings(true)
|
||||
.warnings_into_errors(true)
|
||||
.extra_warnings(true)
|
||||
.compile("logger");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
build_xkbcommon()?;
|
||||
build_xkb_keysyms()?;
|
||||
build_logger()?;
|
||||
Ok(())
|
||||
}
|
||||
17
libxkbcommonrs/ffi/logger.c
Normal file
17
libxkbcommonrs/ffi/logger.c
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "logger.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define buf_size 256
|
||||
static callback_type global_callback = NULL;
|
||||
|
||||
void log_set_callback(callback_type callback) { global_callback = callback; }
|
||||
|
||||
void log_logger(struct xkb_context *context, enum xkb_log_level level,
|
||||
const char *format, va_list args) {
|
||||
if (global_callback) {
|
||||
char buf[buf_size] = {0};
|
||||
vsnprintf(buf, buf_size, format, args);
|
||||
global_callback(context, level, buf);
|
||||
}
|
||||
}
|
||||
11
libxkbcommonrs/ffi/logger.h
Normal file
11
libxkbcommonrs/ffi/logger.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#include <stdarg.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
typedef void (*callback_type)(struct xkb_context *context,
|
||||
enum xkb_log_level level, const char *message);
|
||||
|
||||
void log_set_callback(callback_type callback);
|
||||
|
||||
/// xkb log callback wrapper for printf formatting
|
||||
void log_logger(struct xkb_context *context, enum xkb_log_level level,
|
||||
const char *format, va_list args);
|
||||
65
libxkbcommonrs/src/context.rs
Normal file
65
libxkbcommonrs/src/context.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use super::{
|
||||
error,
|
||||
ffi::{self, context},
|
||||
};
|
||||
|
||||
use std::{ffi::CStr, ptr::NonNull};
|
||||
|
||||
pub struct Context {
|
||||
ctx: NonNull<ffi::context>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Result<Self, error::Error> {
|
||||
let ctx = NonNull::new(unsafe { ffi::context_new(ffi::context_flags::NoFlags) })
|
||||
.ok_or(error::Error::ContextNewFailed)?;
|
||||
unsafe {
|
||||
let ctx = ctx.as_ptr();
|
||||
if let Some(level) = log::max_level().to_level() {
|
||||
ffi::context_set_log_level(ctx, level.into());
|
||||
ffi::log_set_callback(Some(log_callback));
|
||||
ffi::context_set_log_fn(ctx, Some(ffi::log_logger));
|
||||
} else {
|
||||
ffi::context_set_log_fn(ctx, Some(null_handler))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
|
||||
pub(crate) fn as_raw(&self) -> *mut context {
|
||||
self.ctx.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::context_unref(self.as_raw()) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn null_handler(
|
||||
_ctx: *mut context,
|
||||
_priority: ffi::log_level,
|
||||
_format: *const ::std::os::raw::c_char,
|
||||
_args: *mut ffi::__va_list_tag,
|
||||
) {
|
||||
// no op
|
||||
}
|
||||
|
||||
unsafe extern "C" fn log_callback(
|
||||
_ctx: *mut context,
|
||||
priority: ffi::log_level,
|
||||
str: *const std::os::raw::c_char,
|
||||
) {
|
||||
match unsafe { CStr::from_ptr(str).to_str() } {
|
||||
Ok(str) => {
|
||||
log::log!(
|
||||
target: "libxkbcommon",
|
||||
priority.into(),
|
||||
"{str}"
|
||||
);
|
||||
}
|
||||
Err(e) => log::error!("Failed to log message with severity {priority:?}.\n\t{e}"),
|
||||
}
|
||||
}
|
||||
15
libxkbcommonrs/src/error.rs
Normal file
15
libxkbcommonrs/src/error.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
ContextNewFailed,
|
||||
InvalidKeymap,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(&self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
61
libxkbcommonrs/src/ffi.rs
Normal file
61
libxkbcommonrs/src/ffi.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unused)]
|
||||
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
pub mod syms {
|
||||
include!(concat!(env!("OUT_DIR"), "/libxkbcommon-keysyms.rs"));
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/libxkbcommon.rs"));
|
||||
|
||||
impl From<log_level> for log::Level {
|
||||
fn from(value: log_level) -> Self {
|
||||
match value {
|
||||
log_level::Debug => log::Level::Debug,
|
||||
log_level::Info => log::Level::Info,
|
||||
log_level::Warning => log::Level::Warn,
|
||||
log_level::Error => log::Level::Error,
|
||||
_ => log::Level::Debug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<log::Level> for log_level {
|
||||
fn from(value: log::Level) -> Self {
|
||||
match value {
|
||||
log::Level::Trace | log::Level::Debug => log_level::Debug,
|
||||
log::Level::Info => log_level::Info,
|
||||
log::Level::Warn => log_level::Warning,
|
||||
log::Level::Error => log_level::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Keysym {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut buffer = [0_u8; 32];
|
||||
let written =
|
||||
unsafe { keysym_to_utf8(*self, buffer.as_mut_ptr() as *mut _, buffer.len()) } as usize;
|
||||
let str = std::str::from_utf8(&buffer[0..written]).unwrap_or("<invalid>");
|
||||
write!(f, "{str}")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Keysym {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut buffer = [0_u8; 32];
|
||||
let written =
|
||||
unsafe { keysym_get_name(*self, buffer.as_mut_ptr() as *mut _, buffer.len()) } as usize;
|
||||
let str = std::str::from_utf8(&buffer[0..written]).unwrap_or("<invalid>");
|
||||
write!(f, "{str}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Keysym {
|
||||
fn from(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
133
libxkbcommonrs/src/keymap.rs
Normal file
133
libxkbcommonrs/src/keymap.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::ffi::c_void;
|
||||
use std::{ffi::CStr, ptr::NonNull};
|
||||
|
||||
use crate::{error, ffi, LayoutID, LayoutRef};
|
||||
|
||||
use super::context::Context;
|
||||
|
||||
pub use ffi::keycode_t as Key;
|
||||
pub use ffi::layout_index_t as Level;
|
||||
pub use ffi::Keysym;
|
||||
|
||||
pub type KeyIter<'a> = Box<dyn FnMut(Key) + 'a>;
|
||||
|
||||
pub struct Keymap {
|
||||
keymap: NonNull<ffi::keymap>,
|
||||
format: ffi::keymap_format,
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
pub fn new(ctx: &Context, buffer: &[u8]) -> Result<Self, error::Error> {
|
||||
let format = ffi::keymap_format::TextV1;
|
||||
let keymap = unsafe {
|
||||
ffi::keymap_new_from_buffer(
|
||||
ctx.as_raw(),
|
||||
buffer.as_ptr() as *const i8,
|
||||
buffer.len(),
|
||||
format,
|
||||
ffi::keymap_compile_flags::NoFlags,
|
||||
)
|
||||
};
|
||||
Ok(Self {
|
||||
keymap: NonNull::new(keymap).ok_or(error::Error::InvalidKeymap)?,
|
||||
format,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn as_raw(&self) -> *mut ffi::keymap {
|
||||
self.keymap.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Keymap {
|
||||
pub fn get_layouts(&'a self) -> Vec<LayoutRef<'a>> {
|
||||
let num_layouts = unsafe { ffi::keymap_num_layouts(self.as_raw()) };
|
||||
let mut v = Vec::with_capacity(num_layouts as usize);
|
||||
|
||||
for i in 0..num_layouts {
|
||||
v.push(LayoutRef::new(self, i));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
pub fn get_sims_by_level(
|
||||
&'a self,
|
||||
key: Key,
|
||||
layout: Option<LayoutID>,
|
||||
level: Level,
|
||||
) -> &'a [Keysym] {
|
||||
let mut syms_out: *const Keysym = std::ptr::null_mut();
|
||||
unsafe {
|
||||
let ptr = &mut syms_out as *mut _;
|
||||
let len = ffi::keymap_key_get_syms_by_level(
|
||||
self.as_raw(),
|
||||
key,
|
||||
layout.unwrap_or_default(),
|
||||
level,
|
||||
ptr,
|
||||
);
|
||||
if syms_out.is_null() || !syms_out.is_aligned() {
|
||||
&[]
|
||||
} else {
|
||||
std::slice::from_raw_parts(syms_out, len as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_levels_for_key(&self, key: Key, layout: Option<LayoutID>) -> u32 {
|
||||
unsafe { ffi::keymap_num_levels_for_key(self.as_raw(), key, layout.unwrap_or_default()) }
|
||||
}
|
||||
|
||||
pub fn key_for_each(&'a self, visitor: KeyIter<'a>) {
|
||||
let ptr = (&visitor) as *const _;
|
||||
unsafe {
|
||||
ffi::keymap_key_for_each(self.as_raw(), Some(key_iter), ptr as *mut c_void);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_get_name(&self, key: Key) -> Option<&str> {
|
||||
let ptr = unsafe { ffi::keymap_key_get_name(self.as_raw(), key) };
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.inspect_err(|e| log::warn!("Failed to get name for key {key}: {e}"))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Keymap {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let str = unsafe { CStr::from_ptr(ffi::keymap_get_as_string(self.as_raw(), self.format)) }
|
||||
.to_str()
|
||||
.unwrap();
|
||||
write!(f, "{str}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn utf32_to_keysim(utf32: &[u32]) -> Vec<Keysym> {
|
||||
let mut ret = Vec::with_capacity(utf32.len());
|
||||
for c in utf32 {
|
||||
let ks = unsafe { ffi::utf32_to_keysym(*c) };
|
||||
ret.push(ks);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn keycode_is_legal_ext(key: Key) -> bool {
|
||||
key <= ffi::KEYCODE_MAX
|
||||
}
|
||||
|
||||
pub fn keycode_is_legal_x11(key: Key) -> bool {
|
||||
(8..=255).contains(&key)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn key_iter(
|
||||
_keymap: *mut ffi::keymap,
|
||||
key: ffi::keycode_t,
|
||||
data: *mut ::std::os::raw::c_void,
|
||||
) {
|
||||
let visitor = unsafe { &mut *(data as *mut KeyIter<'_>) };
|
||||
visitor(key);
|
||||
}
|
||||
33
libxkbcommonrs/src/layout.rs
Normal file
33
libxkbcommonrs/src/layout.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::{ffi::CStr, fmt::Display};
|
||||
|
||||
use crate::{ffi, Keymap};
|
||||
|
||||
pub type LayoutID = ffi::layout_index_t;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LayoutRef<'a> {
|
||||
keymap: &'a Keymap,
|
||||
id: ffi::layout_index_t,
|
||||
}
|
||||
|
||||
impl<'a> LayoutRef<'a> {
|
||||
pub fn new(keymap: &'a Keymap, id: ffi::layout_index_t) -> Self {
|
||||
Self { keymap, id }
|
||||
}
|
||||
|
||||
pub fn get_name(&'a self) -> &'a str {
|
||||
unsafe { CStr::from_ptr(ffi::keymap_layout_get_name(self.keymap.as_raw(), self.id)) }
|
||||
.to_str()
|
||||
.unwrap_or("<invalid>")
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> LayoutID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for LayoutRef<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.get_name())
|
||||
}
|
||||
}
|
||||
11
libxkbcommonrs/src/lib.rs
Normal file
11
libxkbcommonrs/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod context;
|
||||
mod error;
|
||||
mod ffi;
|
||||
mod keymap;
|
||||
mod layout;
|
||||
|
||||
pub use context::Context;
|
||||
pub use keymap::*;
|
||||
pub use layout::*;
|
||||
|
||||
pub use ffi::syms;
|
||||
Reference in New Issue
Block a user