mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
First commit
This commit is contained in:
commit
459201bc65
11 changed files with 843 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
Cargo.lock
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Florian Dehau <florian.dehau@telecomnancy.net>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
termion = "1.1.1"
|
||||||
|
bitflags = "0.7"
|
||||||
|
cassowary = "0.2.0"
|
6
Makefile
Normal file
6
Makefile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
test:
|
||||||
|
cargo test
|
||||||
|
watch:
|
||||||
|
watchman-make -p 'src/**/*.rs' -t build -p 'test/**/*.rs' -t test
|
94
examples/prototype.rs
Normal file
94
examples/prototype.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
extern crate tui;
|
||||||
|
extern crate termion;
|
||||||
|
|
||||||
|
use std::thread;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::io::{Write, stdin};
|
||||||
|
|
||||||
|
use termion::event;
|
||||||
|
use termion::input::TermRead;
|
||||||
|
|
||||||
|
use tui::Terminal;
|
||||||
|
use tui::widgets::{Widget, Block, Border};
|
||||||
|
use tui::layout::{Group, Direction, Alignment, Size};
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
name: String,
|
||||||
|
fetching: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Event {
|
||||||
|
Quit,
|
||||||
|
Redraw,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
|
||||||
|
let mut app = App {
|
||||||
|
name: String::from("Test app"),
|
||||||
|
fetching: false,
|
||||||
|
};
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let tx = tx.clone();
|
||||||
|
let stdin = stdin();
|
||||||
|
for c in stdin.keys() {
|
||||||
|
let evt = c.unwrap();
|
||||||
|
match evt {
|
||||||
|
event::Key::Char('q') => {
|
||||||
|
tx.send(Event::Quit).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
event::Key::Char('r') => {
|
||||||
|
tx.send(Event::Redraw).unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut terminal = Terminal::new().unwrap();
|
||||||
|
terminal.clear();
|
||||||
|
terminal.hide_cursor();
|
||||||
|
loop {
|
||||||
|
draw(&mut terminal, &app);
|
||||||
|
let evt = rx.recv().unwrap();
|
||||||
|
match evt {
|
||||||
|
Event::Quit => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Event::Redraw => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.show_cursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(terminal: &mut Terminal, app: &App) {
|
||||||
|
|
||||||
|
let ui = Group::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.chunks(&[Size::Fixed(3.0), Size::Percent(100.0), Size::Fixed(3.0)])
|
||||||
|
.render(&terminal.area(), |chunks| {
|
||||||
|
vec![Block::default()
|
||||||
|
.borders(Border::TOP | Border::BOTTOM)
|
||||||
|
.title("Header")
|
||||||
|
.render(&chunks[0]),
|
||||||
|
Group::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.chunks(&[Size::Percent(50.0), Size::Percent(50.0)])
|
||||||
|
.render(&chunks[1], |chunks| {
|
||||||
|
vec![Block::default()
|
||||||
|
.borders(Border::ALL)
|
||||||
|
.title("Podcasts")
|
||||||
|
.render(&chunks[0]),
|
||||||
|
Block::default()
|
||||||
|
.borders(Border::ALL)
|
||||||
|
.title("Episodes")
|
||||||
|
.render(&chunks[1])]
|
||||||
|
}),
|
||||||
|
Block::default().borders(Border::ALL).title("Footer").render(&chunks[2])]
|
||||||
|
});
|
||||||
|
terminal.render(&ui);
|
||||||
|
}
|
150
src/buffer.rs
Normal file
150
src/buffer.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use layout::Rect;
|
||||||
|
use style::Color;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Cell {
|
||||||
|
pub symbol: char,
|
||||||
|
pub fg: Color,
|
||||||
|
pub bg: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cell {
|
||||||
|
fn default() -> Cell {
|
||||||
|
Cell {
|
||||||
|
symbol: ' ',
|
||||||
|
fg: Color::White,
|
||||||
|
bg: Color::Black,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Buffer {
|
||||||
|
area: Rect,
|
||||||
|
content: Vec<Cell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Buffer {
|
||||||
|
fn default() -> Buffer {
|
||||||
|
Buffer {
|
||||||
|
area: Default::default(),
|
||||||
|
content: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Buffer {
|
||||||
|
pub fn empty(area: Rect) -> Buffer {
|
||||||
|
let cell: Cell = Default::default();
|
||||||
|
Buffer::filled(area, cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filled(area: Rect, cell: Cell) -> Buffer {
|
||||||
|
let size = area.area() as usize;
|
||||||
|
let mut content = Vec::with_capacity(size);
|
||||||
|
for _ in 0..size {
|
||||||
|
content.push(cell.clone());
|
||||||
|
}
|
||||||
|
Buffer {
|
||||||
|
area: area,
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> &[Cell] {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area(&self) -> &Rect {
|
||||||
|
&self.area
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||||
|
let index = (y * self.area.width + x) as usize;
|
||||||
|
debug_assert!(index < self.content.len());
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||||
|
debug_assert!(self.area.width != 0);
|
||||||
|
(i as u16 % self.area.width, i as u16 / self.area.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_pos(&self, x: u16, y: u16) -> Option<(u16, u16)> {
|
||||||
|
let mut nx = x + 1;
|
||||||
|
let mut ny = y;
|
||||||
|
if nx >= self.area.width {
|
||||||
|
nx = 0;
|
||||||
|
ny = y + 1;
|
||||||
|
}
|
||||||
|
if ny >= self.area.height {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((nx, ny))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
self.content[i] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_symbol(&mut self, x: u16, y: u16, symbol: char) {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
self.content[i].symbol = symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_string(&mut self, x: u16, y: u16, string: &str) {
|
||||||
|
let mut cursor = (x, y);
|
||||||
|
for c in string.chars() {
|
||||||
|
let index = self.index_of(cursor.0, cursor.1);
|
||||||
|
self.content[index].symbol = c;
|
||||||
|
match self.next_pos(cursor.0, cursor.1) {
|
||||||
|
Some(c) => {
|
||||||
|
cursor = c;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
&self.content[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, other: &Buffer) {
|
||||||
|
let area = self.area.union(&other.area);
|
||||||
|
let cell: Cell = Default::default();
|
||||||
|
self.content.resize(area.area() as usize, cell.clone());
|
||||||
|
|
||||||
|
// Move original content to the appropriate space
|
||||||
|
let offset_x = self.area.x - area.x;
|
||||||
|
let offset_y = self.area.y - area.y;
|
||||||
|
let size = self.area.area() as usize;
|
||||||
|
for i in (0..size).rev() {
|
||||||
|
let (x, y) = self.pos_of(i);
|
||||||
|
// New index in content
|
||||||
|
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||||
|
self.content[k] = self.content[i].clone();
|
||||||
|
if i != k {
|
||||||
|
self.content[i] = cell.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push content of the other buffer into this one (may erase previous
|
||||||
|
// data)
|
||||||
|
let offset_x = other.area.x - area.x;
|
||||||
|
let offset_y = other.area.y - area.y;
|
||||||
|
let size = other.area.area() as usize;
|
||||||
|
for i in 0..size {
|
||||||
|
let (x, y) = other.pos_of(i);
|
||||||
|
// New index in content
|
||||||
|
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||||
|
self.content[k] = other.content[i].clone();
|
||||||
|
}
|
||||||
|
self.area = area;
|
||||||
|
}
|
||||||
|
}
|
263
src/layout.rs
Normal file
263
src/layout.rs
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
use std::cmp::{min, max};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use cassowary::{Solver, Variable, Constraint};
|
||||||
|
use cassowary::WeightedRelation::*;
|
||||||
|
use cassowary::strength::{WEAK, MEDIUM, STRONG, REQUIRED};
|
||||||
|
|
||||||
|
use buffer::Buffer;
|
||||||
|
|
||||||
|
pub enum Alignment {
|
||||||
|
Top,
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
Bottom,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Direction {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Rect {
|
||||||
|
pub x: u16,
|
||||||
|
pub y: u16,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Rect {
|
||||||
|
fn default() -> Rect {
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area(&self) -> u16 {
|
||||||
|
self.width * self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner(&self, spacing: u16) -> Rect {
|
||||||
|
if self.width - spacing < 0 || self.height - spacing < 0 {
|
||||||
|
Rect::default()
|
||||||
|
} else {
|
||||||
|
Rect {
|
||||||
|
x: self.x + spacing,
|
||||||
|
y: self.y + spacing,
|
||||||
|
width: self.width - 2 * spacing,
|
||||||
|
height: self.height - 2 * spacing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn union(&self, other: &Rect) -> Rect {
|
||||||
|
let x1 = min(self.x, other.x);
|
||||||
|
let y1 = min(self.y, other.y);
|
||||||
|
let x2 = max(self.x + self.width, other.x + other.width);
|
||||||
|
let y2 = max(self.y + self.height, other.y + other.height);
|
||||||
|
Rect {
|
||||||
|
x: x1,
|
||||||
|
y: y1,
|
||||||
|
width: x2 - x1,
|
||||||
|
height: y2 - y1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersection(&self, other: &Rect) -> Rect {
|
||||||
|
let x1 = max(self.x, other.x);
|
||||||
|
let y1 = max(self.y, other.y);
|
||||||
|
let x2 = min(self.x + self.width, other.x + other.width);
|
||||||
|
let y2 = min(self.y + self.height, other.y + other.height);
|
||||||
|
Rect {
|
||||||
|
x: x1,
|
||||||
|
y: y1,
|
||||||
|
width: x2 - x1,
|
||||||
|
height: y2 - y1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersects(&self, other: &Rect) -> bool {
|
||||||
|
self.x < other.x + other.width && self.x + self.width > other.x &&
|
||||||
|
self.y < other.y + other.height && self.y + self.height > other.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Size {
|
||||||
|
Fixed(f64),
|
||||||
|
Percent(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// extern crate tui;
|
||||||
|
/// use tui::layout::{Rect, Size, Alignment, Direction, split};
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// let chunks = split(&Rect{x: 2, y: 2, width: 10, height: 10}, Direction::Vertical,
|
||||||
|
/// Alignment::Left, &[Size::Fixed(5.0), Size::Percent(80.0)]);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) -> Vec<Rect> {
|
||||||
|
let mut solver = Solver::new();
|
||||||
|
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
|
||||||
|
let elements = sizes.iter().map(|e| Element::new()).collect::<Vec<Element>>();
|
||||||
|
let mut results = sizes.iter().map(|e| Rect::default()).collect::<Vec<Rect>>();
|
||||||
|
for (i, e) in elements.iter().enumerate() {
|
||||||
|
vars.insert(e.x, (i, 0));
|
||||||
|
vars.insert(e.y, (i, 1));
|
||||||
|
vars.insert(e.width, (i, 2));
|
||||||
|
vars.insert(e.height, (i, 3));
|
||||||
|
}
|
||||||
|
let mut constraints: Vec<Constraint> = Vec::new();
|
||||||
|
if let Some(size) = sizes.first() {
|
||||||
|
constraints.push(match *dir {
|
||||||
|
Direction::Horizontal => elements[0].x | EQ(REQUIRED) | area.x as f64,
|
||||||
|
Direction::Vertical => elements[0].y | EQ(REQUIRED) | area.y as f64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if let Some(size) = sizes.last() {
|
||||||
|
let last = elements.last().unwrap();
|
||||||
|
constraints.push(match *dir {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
last.x + last.width | EQ(REQUIRED) | (area.x + area.width) as f64
|
||||||
|
}
|
||||||
|
Direction::Vertical => {
|
||||||
|
last.y + last.height | EQ(REQUIRED) | (area.y + area.height) as f64
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
match *dir {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
constraints.push(pair[0].x + pair[0].width | LE(REQUIRED) | pair[1].x);
|
||||||
|
}
|
||||||
|
for (i, size) in sizes.iter().enumerate() {
|
||||||
|
let cs = [elements[i].y | EQ(REQUIRED) | area.y as f64,
|
||||||
|
elements[i].height | EQ(REQUIRED) | area.height as f64,
|
||||||
|
match *size {
|
||||||
|
Size::Fixed(f) => elements[i].width | EQ(REQUIRED) | f,
|
||||||
|
Size::Percent(p) => {
|
||||||
|
elements[i].width | EQ(WEAK) | area.width as f64 * p / 100.0
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
constraints.extend_from_slice(&cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Direction::Vertical => {
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
constraints.push(pair[0].y + pair[0].height | LE(REQUIRED) | pair[1].y);
|
||||||
|
}
|
||||||
|
for (i, size) in sizes.iter().enumerate() {
|
||||||
|
let cs = [elements[i].x | EQ(REQUIRED) | area.x as f64,
|
||||||
|
elements[i].width | EQ(REQUIRED) | area.width as f64,
|
||||||
|
match *size {
|
||||||
|
Size::Fixed(f) => elements[i].height | EQ(REQUIRED) | f,
|
||||||
|
Size::Percent(p) => {
|
||||||
|
elements[i].height | EQ(WEAK) | area.height as f64 * p / 100.0
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
constraints.extend_from_slice(&cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
solver.add_constraints(&constraints).unwrap();
|
||||||
|
for &(var, value) in solver.fetch_changes() {
|
||||||
|
let (index, attr) = vars[&var];
|
||||||
|
match attr {
|
||||||
|
0 => {
|
||||||
|
results[index].x = value as u16;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
results[index].y = value as u16;
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
results[index].width = value as u16;
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
results[index].height = value as u16;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Element {
|
||||||
|
x: Variable,
|
||||||
|
y: Variable,
|
||||||
|
width: Variable,
|
||||||
|
height: Variable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
fn new() -> Element {
|
||||||
|
Element {
|
||||||
|
x: Variable::new(),
|
||||||
|
y: Variable::new(),
|
||||||
|
width: Variable::new(),
|
||||||
|
height: Variable::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Group {
|
||||||
|
direction: Direction,
|
||||||
|
alignment: Alignment,
|
||||||
|
chunks: Vec<Size>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Group {
|
||||||
|
fn default() -> Group {
|
||||||
|
Group {
|
||||||
|
direction: Direction::Horizontal,
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
chunks: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Group {
|
||||||
|
pub fn direction(&mut self, direction: Direction) -> &mut Group {
|
||||||
|
self.direction = direction;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alignment(&mut self, alignment: Alignment) -> &mut Group {
|
||||||
|
self.alignment = alignment;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunks(&mut self, chunks: &[Size]) -> &mut Group {
|
||||||
|
self.chunks = Vec::from(chunks);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn render<F>(&self, area: &Rect, f: F) -> Buffer
|
||||||
|
where F: Fn(&[Rect]) -> Vec<Buffer>
|
||||||
|
{
|
||||||
|
let chunks = split(area, &self.direction, &self.alignment, &self.chunks);
|
||||||
|
let results = f(&chunks);
|
||||||
|
let mut result = results[0].clone();
|
||||||
|
for r in results.iter().skip(1) {
|
||||||
|
result.merge(&r);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
18
src/lib.rs
Normal file
18
src/lib.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
extern crate termion;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate bitflags;
|
||||||
|
extern crate cassowary;
|
||||||
|
|
||||||
|
mod buffer;
|
||||||
|
pub mod terminal;
|
||||||
|
pub mod widgets;
|
||||||
|
pub mod style;
|
||||||
|
pub mod layout;
|
||||||
|
|
||||||
|
pub use self::terminal::Terminal;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn it_works() {}
|
||||||
|
}
|
61
src/style.rs
Normal file
61
src/style.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use termion;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Color {
|
||||||
|
Black,
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Yellow,
|
||||||
|
Magenta,
|
||||||
|
Cyan,
|
||||||
|
Gray,
|
||||||
|
DarkGray,
|
||||||
|
LightRed,
|
||||||
|
LightGreen,
|
||||||
|
LightYellow,
|
||||||
|
LightMagenta,
|
||||||
|
LightCyan,
|
||||||
|
White,
|
||||||
|
Rgb(u8, u8, u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub fn fg(&self) -> String {
|
||||||
|
match *self {
|
||||||
|
Color::Black => format!("{}", termion::color::Fg(termion::color::Black)),
|
||||||
|
Color::Red => format!("{}", termion::color::Fg(termion::color::Red)),
|
||||||
|
Color::Green => format!("{}", termion::color::Fg(termion::color::Green)),
|
||||||
|
Color::Yellow => format!("{}", termion::color::Fg(termion::color::Yellow)),
|
||||||
|
Color::Magenta => format!("{}", termion::color::Fg(termion::color::Magenta)),
|
||||||
|
Color::Cyan => format!("{}", termion::color::Fg(termion::color::Cyan)),
|
||||||
|
Color::Gray => format!("{}", termion::color::Fg(termion::color::Rgb(146, 131, 116))),
|
||||||
|
Color::DarkGray => format!("{}", termion::color::Fg(termion::color::Rgb(80, 73, 69))),
|
||||||
|
Color::LightRed => format!("{}", termion::color::Fg(termion::color::LightRed)),
|
||||||
|
Color::LightGreen => format!("{}", termion::color::Fg(termion::color::LightGreen)),
|
||||||
|
Color::LightYellow => format!("{}", termion::color::Fg(termion::color::LightYellow)),
|
||||||
|
Color::LightMagenta => format!("{}", termion::color::Fg(termion::color::LightMagenta)),
|
||||||
|
Color::LightCyan => format!("{}", termion::color::Fg(termion::color::LightCyan)),
|
||||||
|
Color::White => format!("{}", termion::color::Fg(termion::color::White)),
|
||||||
|
Color::Rgb(r, g, b) => format!("{}", termion::color::Fg(termion::color::Rgb(r, g, b))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn bg(&self) -> String {
|
||||||
|
match *self {
|
||||||
|
Color::Black => format!("{}", termion::color::Bg(termion::color::Black)),
|
||||||
|
Color::Red => format!("{}", termion::color::Bg(termion::color::Red)),
|
||||||
|
Color::Green => format!("{}", termion::color::Bg(termion::color::Green)),
|
||||||
|
Color::Yellow => format!("{}", termion::color::Bg(termion::color::Yellow)),
|
||||||
|
Color::Magenta => format!("{}", termion::color::Bg(termion::color::Magenta)),
|
||||||
|
Color::Cyan => format!("{}", termion::color::Bg(termion::color::Cyan)),
|
||||||
|
Color::Gray => format!("{}", termion::color::Bg(termion::color::Rgb(146, 131, 116))),
|
||||||
|
Color::DarkGray => format!("{}", termion::color::Bg(termion::color::Rgb(80, 73, 69))),
|
||||||
|
Color::LightRed => format!("{}", termion::color::Bg(termion::color::LightRed)),
|
||||||
|
Color::LightGreen => format!("{}", termion::color::Bg(termion::color::LightGreen)),
|
||||||
|
Color::LightYellow => format!("{}", termion::color::Bg(termion::color::LightYellow)),
|
||||||
|
Color::LightMagenta => format!("{}", termion::color::Bg(termion::color::LightMagenta)),
|
||||||
|
Color::LightCyan => format!("{}", termion::color::Bg(termion::color::LightCyan)),
|
||||||
|
Color::White => format!("{}", termion::color::Bg(termion::color::White)),
|
||||||
|
Color::Rgb(r, g, b) => format!("{}", termion::color::Bg(termion::color::Rgb(r, g, b))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
src/terminal.rs
Normal file
64
src/terminal.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
use std::iter;
|
||||||
|
use std::io;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use termion;
|
||||||
|
use termion::raw::{IntoRawMode, RawTerminal};
|
||||||
|
|
||||||
|
use buffer::Buffer;
|
||||||
|
use layout::Rect;
|
||||||
|
|
||||||
|
pub struct Terminal {
|
||||||
|
stdout: RawTerminal<io::Stdout>,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
pub fn new() -> Result<Terminal, io::Error> {
|
||||||
|
let terminal = try!(termion::terminal_size());
|
||||||
|
let stdout = try!(io::stdout().into_raw_mode());
|
||||||
|
Ok(Terminal {
|
||||||
|
stdout: stdout,
|
||||||
|
width: terminal.0,
|
||||||
|
height: terminal.1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area(&self) -> Rect {
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, buffer: &Buffer) {
|
||||||
|
for (i, cell) in buffer.content().iter().enumerate() {
|
||||||
|
let (lx, ly) = buffer.pos_of(i);
|
||||||
|
let (x, y) = (lx + buffer.area().x, ly + buffer.area().y);
|
||||||
|
write!(self.stdout,
|
||||||
|
"{}{}{}{}",
|
||||||
|
termion::cursor::Goto(x + 1, y + 1),
|
||||||
|
cell.fg.fg(),
|
||||||
|
cell.bg.bg(),
|
||||||
|
cell.symbol)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
self.stdout.flush().unwrap();
|
||||||
|
}
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
write!(self.stdout, "{}", termion::clear::All).unwrap();
|
||||||
|
self.stdout.flush().unwrap();
|
||||||
|
}
|
||||||
|
pub fn hide_cursor(&mut self) {
|
||||||
|
write!(self.stdout, "{}", termion::cursor::Hide).unwrap();
|
||||||
|
self.stdout.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_cursor(&mut self) {
|
||||||
|
write!(self.stdout, "{}", termion::cursor::Show).unwrap();
|
||||||
|
self.stdout.flush().unwrap();
|
||||||
|
}
|
||||||
|
}
|
94
src/widgets/block.rs
Normal file
94
src/widgets/block.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
|
||||||
|
use buffer::Buffer;
|
||||||
|
use layout::Rect;
|
||||||
|
use style::Color;
|
||||||
|
use widgets::{Widget, Border, Line, vline, hline};
|
||||||
|
|
||||||
|
pub struct Block<'a> {
|
||||||
|
title: Option<&'a str>,
|
||||||
|
borders: Border::Flags,
|
||||||
|
border_fg: Color,
|
||||||
|
border_bg: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Block<'a> {
|
||||||
|
fn default() -> Block<'a> {
|
||||||
|
Block {
|
||||||
|
title: None,
|
||||||
|
borders: Border::NONE,
|
||||||
|
border_fg: Color::White,
|
||||||
|
border_bg: Color::Black,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Block<'a> {
|
||||||
|
pub fn title(&mut self, title: &'a str) -> &mut Block<'a> {
|
||||||
|
self.title = Some(title);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn borders(&mut self, flag: Border::Flags) -> &mut Block<'a> {
|
||||||
|
self.borders = flag;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Block<'a> {
|
||||||
|
fn render(&self, area: &Rect) -> Buffer {
|
||||||
|
|
||||||
|
let mut buf = Buffer::empty(*area);
|
||||||
|
|
||||||
|
if area.area() == 0 {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.borders == Border::NONE {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sides
|
||||||
|
if self.borders.intersects(Border::LEFT) {
|
||||||
|
let line = vline(area.x, area.y, area.height, self.border_fg, self.border_bg);
|
||||||
|
buf.merge(&line);
|
||||||
|
}
|
||||||
|
if self.borders.intersects(Border::TOP) {
|
||||||
|
let line = hline(area.x, area.y, area.width, self.border_fg, self.border_bg);
|
||||||
|
buf.merge(&line);
|
||||||
|
}
|
||||||
|
if self.borders.intersects(Border::RIGHT) {
|
||||||
|
let line = vline(area.x + area.width - 1,
|
||||||
|
area.y,
|
||||||
|
area.height,
|
||||||
|
self.border_fg,
|
||||||
|
self.border_bg);
|
||||||
|
buf.merge(&line);
|
||||||
|
}
|
||||||
|
if self.borders.intersects(Border::BOTTOM) {
|
||||||
|
let line = hline(area.x,
|
||||||
|
area.y + area.height - 1,
|
||||||
|
area.width,
|
||||||
|
self.border_fg,
|
||||||
|
self.border_bg);
|
||||||
|
buf.merge(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
if self.borders.contains(Border::LEFT | Border::TOP) {
|
||||||
|
buf.set_symbol(0, 0, Line::TopLeft.get());
|
||||||
|
}
|
||||||
|
if self.borders.contains(Border::RIGHT | Border::TOP) {
|
||||||
|
buf.set_symbol(area.width - 1, 0, Line::TopRight.get());
|
||||||
|
}
|
||||||
|
if self.borders.contains(Border::BOTTOM | Border::LEFT) {
|
||||||
|
buf.set_symbol(0, area.height - 1, Line::BottomLeft.get());
|
||||||
|
}
|
||||||
|
if self.borders.contains(Border::BOTTOM | Border::RIGHT) {
|
||||||
|
buf.set_symbol(area.width - 1, area.height - 1, Line::BottomRight.get());
|
||||||
|
}
|
||||||
|
if let Some(ref title) = self.title {
|
||||||
|
buf.set_string(1, 0, &format!(" {} ", title));
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
82
src/widgets/mod.rs
Normal file
82
src/widgets/mod.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
mod block;
|
||||||
|
|
||||||
|
pub use self::block::Block;
|
||||||
|
|
||||||
|
use buffer::{Buffer, Cell};
|
||||||
|
use layout::Rect;
|
||||||
|
use style::Color;
|
||||||
|
|
||||||
|
enum Line {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomRight,
|
||||||
|
VerticalLeft,
|
||||||
|
VerticalRight,
|
||||||
|
HorizontalDown,
|
||||||
|
HorizontalUp,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod Border {
|
||||||
|
bitflags! {
|
||||||
|
pub flags Flags: u32 {
|
||||||
|
const NONE = 0b00000001,
|
||||||
|
const TOP = 0b00000010,
|
||||||
|
const RIGHT = 0b00000100,
|
||||||
|
const BOTTOM = 0b0001000,
|
||||||
|
const LEFT = 0b00010000,
|
||||||
|
const ALL = TOP.bits | RIGHT.bits | BOTTOM.bits | LEFT.bits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
fn get<'a>(&self) -> char {
|
||||||
|
match *self {
|
||||||
|
Line::TopRight => '┐',
|
||||||
|
Line::Vertical => '│',
|
||||||
|
Line::Horizontal => '─',
|
||||||
|
Line::TopLeft => '┌',
|
||||||
|
Line::BottomRight => '┘',
|
||||||
|
Line::BottomLeft => '└',
|
||||||
|
Line::VerticalLeft => '┤',
|
||||||
|
Line::VerticalRight => '├',
|
||||||
|
Line::HorizontalDown => '┬',
|
||||||
|
Line::HorizontalUp => '┴',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn hline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
|
||||||
|
Buffer::filled(Rect {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
width: len,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
Cell {
|
||||||
|
symbol: Line::Horizontal.get(),
|
||||||
|
fg: fg,
|
||||||
|
bg: bg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
|
||||||
|
Buffer::filled(Rect {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
width: 1,
|
||||||
|
height: len,
|
||||||
|
},
|
||||||
|
Cell {
|
||||||
|
symbol: Line::Vertical.get(),
|
||||||
|
fg: fg,
|
||||||
|
bg: bg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Widget {
|
||||||
|
fn render(&self, area: &Rect) -> Buffer;
|
||||||
|
}
|
Loading…
Reference in a new issue