Convert theming to Rust.

This commit is contained in:
Emmanuel Gil Peyrot 2018-10-17 04:37:05 +02:00 committed by Maxime “pep” Buquet
parent 6378f6ee6b
commit eef1d2041d
5 changed files with 221 additions and 76 deletions

View file

@ -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"]

2
poezio/libpoezio.pyi Normal file
View file

@ -0,0 +1,2 @@
def to_curses_attr(fg: int, bg: int, attrs: str) -> int: ...

View file

@ -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:
"""

View file

@ -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::<LogParseError>().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<PyObject> {
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))
}

161
src/theming.rs Normal file
View file

@ -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<Attr>>,
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<i16> = 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<HashMap<(i16, i16), i16>> = {
Mutex::new(HashMap::new())
};
static ref TABLE_256_TO_16: Vec<u8> = 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>) -> 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);
}
}