diff --git a/Cargo.toml b/Cargo.toml index 3beed930..27257012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,11 @@ authors = [ [dependencies] cpython = "0.7" +nom = "4" +chrono = "0.4" +ncurses = "5" +lazy_static = "1" +enum-set = "0.0" [lib] crate-type = ["cdylib"] diff --git a/poezio/libpoezio.pyi b/poezio/libpoezio.pyi new file mode 100644 index 00000000..e02e0a0f --- /dev/null +++ b/poezio/libpoezio.pyi @@ -0,0 +1,2 @@ + +def to_curses_attr(fg: int, bg: int, attrs: str) -> int: ... diff --git a/poezio/theming.py b/poezio/theming.py index 712a44ab..446455e0 100755 --- a/poezio/theming.py +++ b/poezio/theming.py @@ -76,8 +76,8 @@ import functools from typing import Dict, List, Union, Tuple, Optional, cast from pathlib import Path from os import path -from poezio import colors, xdg from datetime import datetime +from poezio import colors, xdg, libpoezio from importlib import machinery finder = machinery.PathFinder() @@ -399,43 +399,9 @@ class Theme: # This is the default theme object, used if no theme is defined in the conf theme = Theme() -# a dict "color tuple -> color_pair" -# Each time we use a color tuple, we check if it has already been used. -# If not we create a new color_pair and keep it in that dict, to use it -# the next time. -curses_colors_dict: Dict[Union[Tuple[int, int], Tuple[int, int, str]], int] = {} - -# yapf: disable - -table_256_to_16 = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, - 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, - 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, - 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, - 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, - 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12, - 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5, - 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, - 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, - 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, - 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10, - 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, - 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, - 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8, - 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15 -] -# yapf: enable - load_path: List[str] = [] -def color_256_to_16(color): - if color == -1: - return color - return table_256_to_16[color] - - def dump_tuple(tup: Union[Tuple[int, int], Tuple[int, int, str]]) -> str: """ Dump a tuple to a string of fg,bg,attr (optional) @@ -454,52 +420,14 @@ def read_tuple(_str: str) -> Tuple[Tuple[int, int], str]: @functools.lru_cache(maxsize=128) def to_curses_attr( - color_tuple: Union[Tuple[int, int], Tuple[int, int, str]]) -> int: + ccolors: Union[Tuple[int, int], Tuple[int, int, str]]) -> int: """ Takes a color tuple (as defined at the top of this file) and returns a valid curses attr that can be passed directly to attron() or attroff() """ # extract the color from that tuple - colors: Union[Tuple[int, int], Tuple[int, int, str]] - if len(color_tuple) == 3: - colors = (color_tuple[0], color_tuple[1]) - else: - colors = color_tuple - - bold = False - if curses.COLORS < 256: - # We are not in a term supporting 256 colors, so we convert - # colors to numbers between -1 and 8 - colors = (color_256_to_16(colors[0]), color_256_to_16(colors[1])) - if colors[0] >= 8: - colors = (colors[0] - 8, colors[1]) - bold = True - if colors[1] >= 8: - colors = (colors[0], colors[1] - 8) - - # check if we already used these colors - try: - pair = curses_colors_dict[colors] - except KeyError: - pair = len(curses_colors_dict) + 1 - curses.init_pair(pair, colors[0], colors[1]) - curses_colors_dict[colors] = pair - curses_pair = curses.color_pair(pair) - if len(color_tuple) == 3: - _, _, additional_val = cast(Tuple[int, int, str], color_tuple) - if 'b' in additional_val or bold is True: - curses_pair = curses_pair | curses.A_BOLD - if 'u' in additional_val: - curses_pair = curses_pair | curses.A_UNDERLINE - if 'i' in additional_val: - curses_pair = curses_pair | (curses.A_ITALIC if hasattr( - curses, 'A_ITALIC') else curses.A_REVERSE) - if 'a' in additional_val: - curses_pair = curses_pair | curses.A_BLINK - if 'r' in additional_val: - curses_pair = curses_pair | curses.A_REVERSE - return curses_pair - + attrs = '' if len(ccolors) < 3 else ccolors[2] # type: ignore + return libpoezio.to_curses_attr(ccolors[0], ccolors[1], attrs) def get_theme() -> Theme: """ diff --git a/src/lib.rs b/src/lib.rs index d261b89e..fae54570 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,55 @@ #[macro_use] extern crate cpython; +#[macro_use] +extern crate nom; +extern crate ncurses; +#[macro_use] +extern crate lazy_static; +extern crate enum_set; + +pub mod theming; + +use self::theming::{curses_attr, parse_attrs}; +use cpython::{PyErr, PyObject, PyResult, Python, PythonObject, ToPyObject}; py_module_initializer!(libpoezio, initlibpoezio, PyInit_libpoezio, |py, m| { + m.add( + py, + "to_curses_attr", + py_fn!(py, to_curses_attr(fg: i16, bg: i16, attrs: &str)), + )?; Ok(()) }); + +py_exception!(libpoezio, LogParseError); + +macro_rules! py_int { + ($py:ident, $i:expr) => { + $i.to_py_object($py).into_object() + }; +} + +fn nom_to_py_err(py: Python, err: nom::Err<&str>) -> PyErr { + PyErr { + ptype: py.get_type::().into_object(), + pvalue: Some( + LogParseError( + err.into_error_kind() + .description() + .to_py_object(py) + .into_object(), + ) + .into_object(), + ), + ptraceback: None, + } +} + +fn to_curses_attr(py: Python, fg: i16, bg: i16, attrs: &str) -> PyResult { + let attrs = match parse_attrs(attrs) { + Ok(attrs) => attrs.1, + Err(err) => return Err(nom_to_py_err(py, err)), + }; + let result = curses_attr(fg, bg, attrs); + Ok(py_int!(py, result)) +} diff --git a/src/theming.rs b/src/theming.rs new file mode 100644 index 00000000..ff108663 --- /dev/null +++ b/src/theming.rs @@ -0,0 +1,161 @@ +use enum_set::{CLike, EnumSet}; +use ncurses::{attr_t, init_pair, A_BLINK, A_BOLD, A_ITALIC, A_UNDERLINE, COLORS, COLOR_PAIR}; +use std::collections::HashMap; +use std::mem; +use std::sync::Mutex; + +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u32)] +pub enum Attr { + Bold, + Italic, + Underline, + Blink, +} + +impl Attr { + pub fn get_attron(&self) -> attr_t { + match *self { + Attr::Bold => A_BOLD(), + Attr::Italic => A_ITALIC(), + Attr::Underline => A_UNDERLINE(), + Attr::Blink => A_BLINK(), + } + } +} + +impl CLike for Attr { + fn to_u32(&self) -> u32 { + *self as u32 + } + + unsafe fn from_u32(v: u32) -> Self { + mem::transmute(v) + } +} + +named!( + pub(crate) parse_attrs<&str, EnumSet>, + do_parse!( + vec: many0!(alt_complete!( + tag!("b") => { |_| Attr::Bold } | + tag!("i") => { |_| Attr::Italic } | + tag!("u") => { |_| Attr::Underline } | + tag!("a") => { |_| Attr::Blink } + )) >> + ({ + let mut set = EnumSet::new(); + set.extend(vec); + set + }) + ) +); + +lazy_static! { + // TODO: probably replace that mutex with an atomic. + static ref NEXT_PAIR: Mutex = Mutex::new(1); + + /// a dict "color tuple -> color_pair" + /// Each time we use a color tuple, we check if it has already been used. + /// If not we create a new color_pair and keep it in that dict, to use it + /// the next time. + static ref COLOURS_DICT: Mutex> = { + Mutex::new(HashMap::new()) + }; + + static ref TABLE_256_TO_16: Vec = vec![ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, + 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, + 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, + 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, + 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, + 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12, + 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5, + 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, + 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, + 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, + 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10, + 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, + 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, + 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8, + 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15 + ]; +} + +fn colour_256_to_16(colour: i16) -> i16 { + if colour == -1 { + return -1; + } + return TABLE_256_TO_16[colour as usize] as i16; +} + +fn get_pair(fg: i16, bg: i16) -> attr_t { + let mut dict = COLOURS_DICT.lock().unwrap(); + if let Some(val) = dict.get(&(fg, bg)) { + return COLOR_PAIR(*val); + } + let mut pair_mut = NEXT_PAIR.lock().unwrap(); + let pair = *pair_mut; + init_pair(pair, fg, bg); + dict.insert((fg, bg), pair); + *pair_mut += 1; + COLOR_PAIR(pair) +} + +/// Takes a color tuple (as defined at the top of this file) and +/// returns a valid curses attr that can be passed directly to attron() or attroff() +pub fn curses_attr(mut fg: i16, mut bg: i16, mut attrs: EnumSet) -> attr_t { + if COLORS() < 256 { + // We are not in a term supporting 256 colors, so we convert + // colors to numbers between -1 and 8. + fg = colour_256_to_16(fg); + bg = colour_256_to_16(bg); + if fg >= 8 { + fg -= 8; + attrs.insert(Attr::Bold); + } + if bg >= 8 { + bg -= 8; + } + }; + let mut pair = get_pair(fg, bg); + for attr in attrs.iter() { + pair |= attr.get_attron(); + } + pair +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn none() { + let attrs = ""; + let expected = EnumSet::new(); + let received = parse_attrs(attrs).unwrap(); + assert_eq!(received.1, expected); + } + + #[test] + fn bold_twice() { + let attrs = "bb"; + let mut expected = EnumSet::new(); + expected.insert(Attr::Bold); + let received = parse_attrs(attrs).unwrap(); + assert_eq!(received.1, expected); + } + + #[test] + fn all() { + let attrs = "baiu"; + let mut expected = EnumSet::new(); + expected.insert(Attr::Bold); + expected.insert(Attr::Blink); + expected.insert(Attr::Italic); + expected.insert(Attr::Underline); + let received = parse_attrs(attrs).unwrap(); + assert_eq!(received.1, expected); + } +}