mirror of https://github.com/helix-editor/helix
Port to termwiz: compiles but no rendering yet
parent
20a132e36f
commit
7a51085e8a
@ -1,197 +0,0 @@
|
||||
use crate::{backend::Backend, buffer::Cell};
|
||||
use crossterm::{
|
||||
cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
};
|
||||
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(buffer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.buffer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
self.buffer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
))
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
|
||||
let shape = match kind {
|
||||
CursorKind::Block => CursorShape::Block,
|
||||
CursorKind::Bar => CursorShape::Line,
|
||||
CursorKind::Underline => CursorShape::UnderScore,
|
||||
CursorKind::Hidden => unreachable!(),
|
||||
};
|
||||
map_error(execute!(self.buffer, Show, SetCursorShape(shape)))
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Clear(ClearType::All)))
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use crate::buffer::Cell;
|
||||
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
pub trait Backend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> Result<(), io::Error>;
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::{Buffer, Cell},
|
||||
};
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use std::{fmt::Write, io};
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
#[derive(Debug)]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
height: u16,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Returns a string representation of the given buffer for debugging purpose.
|
||||
fn buffer_view(buffer: &Buffer) -> String {
|
||||
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
|
||||
for cells in buffer.content.chunks(buffer.area.width as usize) {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
view.push('"');
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol))
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
write!(
|
||||
&mut view,
|
||||
" Hidden by multi-width symbols: {:?}",
|
||||
overwritten
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
view.push('\n');
|
||||
}
|
||||
view
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
width,
|
||||
height,
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_eq!(expected.area, self.buffer.area);
|
||||
let diff = expected.diff(&self.buffer);
|
||||
if diff.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut debug_info = String::from("Buffers are not equal");
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Expected:");
|
||||
debug_info.push('\n');
|
||||
let expected_view = buffer_view(expected);
|
||||
debug_info.push_str(&expected_view);
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Got:");
|
||||
debug_info.push('\n');
|
||||
let view = buffer_view(&self.buffer);
|
||||
debug_info.push_str(&view);
|
||||
debug_info.push('\n');
|
||||
|
||||
debug_info.push_str("Diff:");
|
||||
debug_info.push('\n');
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!(
|
||||
"{}: at ({}, {}) expected {:?} got {:?}",
|
||||
i, x, y, expected_cell, cell
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{}", debug_info);
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, c) in content {
|
||||
self.buffer[(x, y)] = c.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self, _kind: CursorKind) -> Result<(), io::Error> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,225 +0,0 @@
|
||||
use crate::{backend::Backend, buffer::Buffer};
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
enum ResizeBehavior {
|
||||
Fixed,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
pub struct Viewport {
|
||||
area: Rect,
|
||||
resize_behavior: ResizeBehavior,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// UNSTABLE
|
||||
pub fn fixed(area: Rect) -> Viewport {
|
||||
Viewport {
|
||||
area,
|
||||
resize_behavior: ResizeBehavior::Fixed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Kind of cursor (hidden or others)
|
||||
cursor_kind: CursorKind,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.cursor_kind == CursorKind::Hidden {
|
||||
if let Err(err) = self.show_cursor(CursorKind::Block) {
|
||||
eprintln!("Failed to show the cursor: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport {
|
||||
area: size,
|
||||
resize_behavior: ResizeBehavior::Auto,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// UNSTABLE
|
||||
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [
|
||||
Buffer::empty(options.viewport.area),
|
||||
Buffer::empty(options.viewport.area),
|
||||
],
|
||||
current: 0,
|
||||
cursor_kind: CursorKind::Block,
|
||||
viewport: options.viewport,
|
||||
})
|
||||
}
|
||||
|
||||
// /// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
// pub fn get_frame(&mut self) -> Frame<B> {
|
||||
// Frame {
|
||||
// terminal: self,
|
||||
// cursor_position: None,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport.area = area;
|
||||
self.clear()
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<Rect> {
|
||||
let size = self.size()?;
|
||||
if size != self.viewport.area {
|
||||
self.resize(size)?;
|
||||
};
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
cursor_kind: CursorKind,
|
||||
) -> io::Result<()> {
|
||||
// // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// // and the terminal (if growing), which may OOB.
|
||||
// self.autoresize()?;
|
||||
|
||||
// let mut frame = self.get_frame();
|
||||
// f(&mut frame);
|
||||
// // We can't change the cursor position right away because we have to flush the frame to
|
||||
// // stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
// let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
if let Some((x, y)) = cursor_position {
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
|
||||
match cursor_kind {
|
||||
CursorKind::Hidden => self.hide_cursor()?,
|
||||
kind => self.show_cursor(kind)?,
|
||||
}
|
||||
|
||||
// Swap buffers
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cursor_kind(&self) -> CursorKind {
|
||||
self.cursor_kind
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.cursor_kind = CursorKind::Hidden;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
|
||||
self.backend.show_cursor(kind)?;
|
||||
self.cursor_kind = kind;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()?;
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue