1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-22 22:42:48 +03:00

termwiz: allow using terminfo on Windows

This commit changes the behavior on Windows:

* If $TERM is set and the `terminfo` crate is able to
  successfully initialize and locate a terminfo database (this also
  requires that $TERMINFO be set in the environment), then we'll
  use the `TerminfoRenderer` instead of the `WindowsConsoleRenderer`
* If $TERM is set to `xterm-256color` and no terminfo database was
  found, use our modern compiled-in copy (look in the `termwiz/data/`
  directory for the source and compiled version of this) and use
  the `TerminfoRenderer`.
* Otherwise use the `WindowsConsoleRenderer`.

In practice, this allows termwiz apps to opt in to features such as
true color support on Windows 10 build 1903 an later by setting their
`TERM=xterm-256color`.   This happens to be the default behavior when
`ssh`ing in to a windows host via `wezterm`.

You can see the truecolor mode get applied by running this example:

```
cargo run --example widgets_basic --features widgets
```

with TERM set as above the background region that is painted by the app
will be a blueish/purplish color, but with it unset or set to something
invalid, it will fall back to black.

I'd like to eventually make termwiz assume the equivalent configuration
to `TERM=xterm-256color` by default on Windows 10 build 1903 and later,
but it's worth getting some feedback on how this works for clients such
as `streampager`.

cc: @quark-zju and @markbt
This commit is contained in:
Wez Furlong 2020-04-04 17:32:39 -07:00
parent fb6d0bbc7c
commit aaf3a7fcaf
9 changed files with 111 additions and 44 deletions

2
Cargo.lock generated
View File

@ -2982,7 +2982,7 @@ dependencies = [
[[package]]
name = "termwiz"
version = "0.7.1"
version = "0.8.0"
dependencies = [
"anyhow",
"base64 0.10.1",

View File

@ -9,4 +9,4 @@ license = "MIT"
documentation = "https://docs.rs/tabout"
[dependencies]
termwiz = { path = "../termwiz", version="0.7"}
termwiz = { path = "../termwiz", version="0.8"}

View File

@ -1,7 +1,7 @@
[package]
authors = ["Wez Furlong"]
name = "termwiz"
version = "0.7.1"
version = "0.8.0"
edition = "2018"
repository = "https://github.com/wez/wezterm"
description = "Terminal Wizardry for Unix and Windows"

View File

@ -167,6 +167,22 @@ impl Capabilities {
Self::new_with_hints(ProbeHints::new_from_env())
}
/// Return modified capabilities with the assumption that we're
/// using an xterm compatible terminal and the built-in xterm
/// terminfo database. This is used on Windows when the TERM
/// is set to xterm-256color and we didn't find an equivalent
/// terminfo on the local filesystem. We're using this as a
/// way to opt in to using terminal escapes rather than the
/// legacy win32 console API.
#[cfg(windows)]
pub(crate) fn apply_builtin_terminfo(mut self) -> Self {
let data = include_bytes!("../../data/xterm-256color");
let db = terminfo::Database::from_buffer(data.as_ref()).unwrap();
self.terminfo_db = Some(db);
self.color_level = ColorLevel::TrueColor;
self
}
/// Build a `Capabilities` object based on the provided `ProbeHints` object.
pub fn new_with_hints(hints: ProbeHints) -> Result<Self, Error> {
let color_level = hints.color_level.unwrap_or_else(|| {

View File

@ -1,4 +1,8 @@
#[cfg(unix)]
pub mod terminfo;
#[cfg(windows)]
pub mod windows;
pub trait RenderTty: std::io::Write {
/// Returns the (cols, rows) for the terminal
fn get_size_in_cells(&mut self) -> anyhow::Result<(usize, usize)>;
}

View File

@ -6,10 +6,10 @@ use crate::escape::csi::{Cursor, Edit, EraseInDisplay, EraseInLine, Sgr, CSI};
use crate::escape::osc::{ITermDimension, ITermFileData, ITermProprietary, OperatingSystemCommand};
use crate::escape::OneBased;
use crate::image::TextureCoordinate;
use crate::render::RenderTty;
use crate::surface::{Change, CursorShape, Position};
use crate::terminal::unix::UnixTty;
use log::error;
use std::io::{Read, Write};
use std::io::Write;
use terminfo::{capability as cap, Capability as TermInfoCapability};
pub struct TerminfoRenderer {
@ -48,7 +48,7 @@ impl TerminfoRenderer {
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::cognitive_complexity))]
fn flush_pending_attr<W: UnixTty + Write>(&mut self, out: &mut W) -> anyhow::Result<()> {
fn flush_pending_attr<W: RenderTty + Write>(&mut self, out: &mut W) -> anyhow::Result<()> {
macro_rules! attr_on {
($cap:ident, $sgr:expr) => {
if let Some(attr) = self.get_capability::<cap::$cap>() {
@ -205,7 +205,7 @@ impl TerminfoRenderer {
Ok(())
}
fn cursor_up<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
fn cursor_up<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if let Some(attr) = self.get_capability::<cap::ParmUpCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
@ -213,7 +213,7 @@ impl TerminfoRenderer {
}
Ok(())
}
fn cursor_down<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
fn cursor_down<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if let Some(attr) = self.get_capability::<cap::ParmDownCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
@ -222,7 +222,7 @@ impl TerminfoRenderer {
Ok(())
}
fn cursor_left<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
fn cursor_left<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if let Some(attr) = self.get_capability::<cap::ParmLeftCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
@ -230,7 +230,7 @@ impl TerminfoRenderer {
}
Ok(())
}
fn cursor_right<W: UnixTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
fn cursor_right<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> anyhow::Result<()> {
if let Some(attr) = self.get_capability::<cap::ParmRightCursor>() {
attr.expand().count(n).to(out.by_ref())?;
} else {
@ -245,10 +245,9 @@ impl TerminfoRenderer {
feature = "cargo-clippy",
allow(clippy::cyclomatic_complexity, clippy::cognitive_complexity)
)]
pub fn render_to<R: Read, W: UnixTty + Write>(
pub fn render_to<W: RenderTty + Write>(
&mut self,
changes: &[Change],
_read: &mut R,
out: &mut W,
) -> anyhow::Result<()> {
macro_rules! record {
@ -315,8 +314,8 @@ impl TerminfoRenderer {
)?;
}
let size = out.get_size()?;
let num_spaces = size.ws_col as usize * size.ws_row as usize;
let (cols, rows) = out.get_size_in_cells()?;
let num_spaces = cols * rows;
let mut buf = Vec::with_capacity(num_spaces);
buf.resize(num_spaces, b' ');
out.write_all(buf.as_slice())?;
@ -725,6 +724,11 @@ mod test {
}
}
}
impl RenderTty for FakeTty {
fn get_size_in_cells(&mut self) -> anyhow::Result<(usize, usize)> {
Ok((self.size.ws_col as usize, self.size.ws_row as usize))
}
}
impl UnixTty for FakeTty {
fn get_size(&mut self) -> anyhow::Result<winsize> {
@ -770,7 +774,6 @@ mod test {
}
struct FakeTerm {
read: FakeTty,
write: FakeTty,
renderer: TerminfoRenderer,
}
@ -781,14 +784,9 @@ mod test {
}
fn new_with_size(caps: Capabilities, width: usize, height: usize) -> Self {
let read = FakeTty::new_with_size(width, height);
let write = FakeTty::new_with_size(width, height);
let renderer = TerminfoRenderer::new(caps);
Self {
read,
write,
renderer,
}
Self { write, renderer }
}
fn parse(&self) -> Vec<Action> {
@ -815,8 +813,7 @@ mod test {
}
fn render(&mut self, changes: &[Change]) -> anyhow::Result<()> {
self.renderer
.render_to(changes, &mut self.read, &mut self.write)
self.renderer.render_to(changes, &mut self.write)
}
fn get_screen_size(&mut self) -> anyhow::Result<ScreenSize> {

View File

@ -4,9 +4,9 @@ use crate::caps::Capabilities;
use crate::cell::{AttributeChange, CellAttributes, Underline};
use crate::color::{AnsiColor, ColorAttribute};
use crate::surface::{Change, Position};
use crate::terminal::windows::{ConsoleInputHandle, ConsoleOutputHandle};
use crate::terminal::windows::ConsoleOutputHandle;
use num;
use std::io::{Read, Write};
use std::io::Write;
use winapi::um::wincon::{
BACKGROUND_BLUE, BACKGROUND_GREEN, BACKGROUND_INTENSITY, BACKGROUND_RED,
COMMON_LVB_REVERSE_VIDEO, COMMON_LVB_UNDERSCORE, FOREGROUND_BLUE, FOREGROUND_GREEN,
@ -111,10 +111,9 @@ fn to_attr_word(attr: &CellAttributes) -> u16 {
}
impl WindowsConsoleRenderer {
pub fn render_to<A: ConsoleInputHandle + Read, B: ConsoleOutputHandle + Write>(
pub fn render_to<B: ConsoleOutputHandle + Write>(
&mut self,
changes: &[Change],
_read: &mut A,
out: &mut B,
) -> anyhow::Result<()> {
for change in changes {

View File

@ -1,3 +1,4 @@
use crate::render::RenderTty;
use anyhow::{anyhow, bail, Context, Error};
use filedescriptor::{poll, pollfd, FileDescriptor, POLLIN};
use libc::{self, winsize};
@ -119,6 +120,13 @@ impl Write for TtyWriteHandle {
}
}
impl RenderTty for TtyWriteHandle {
fn get_size_in_cells(&mut self) -> anyhow::Result<(usize, usize)> {
let size = self.get_size()?;
Ok((size.ws_col as usize, size.ws_row as usize))
}
}
impl UnixTty for TtyWriteHandle {
fn get_size(&mut self) -> Result<winsize, Error> {
let mut size: winsize = unsafe { mem::zeroed() };
@ -366,8 +374,7 @@ impl Terminal for UnixTerminal {
self.write.set_size(size)
}
fn render(&mut self, changes: &[Change]) -> Result<(), Error> {
self.renderer
.render_to(changes, &mut self.read, &mut self.write)
self.renderer.render_to(changes, &mut self.write)
}
fn flush(&mut self) -> Result<(), Error> {
self.write.flush().context("flush failed")

View File

@ -24,12 +24,19 @@ use winapi::um::wincon::{
use crate::caps::Capabilities;
use crate::input::{InputEvent, InputParser};
use crate::render::terminfo::TerminfoRenderer;
use crate::render::windows::WindowsConsoleRenderer;
use crate::render::RenderTty;
use crate::surface::Change;
use crate::terminal::{cast, ScreenSize, Terminal};
const BUF_SIZE: usize = 128;
enum Renderer {
Terminfo(TerminfoRenderer),
Windows(WindowsConsoleRenderer),
}
pub trait ConsoleInputHandle {
fn set_input_mode(&mut self, mode: u32) -> Result<(), Error>;
fn get_input_mode(&mut self) -> Result<u32, Error>;
@ -141,6 +148,26 @@ impl OutputHandle {
}
}
fn dimensions_from_buffer_info(info: CONSOLE_SCREEN_BUFFER_INFO) -> (usize, usize) {
// NOTE: the default console behavior is different from unix style
// terminals wrt. handling printing in the last column position.
// We under report the width by one to make it easier to have similar
// semantics to unix style terminals.
let cols = 0 + (info.srWindow.Right - info.srWindow.Left);
let rows = 1 + (info.srWindow.Bottom - info.srWindow.Top);
(cols as usize, rows as usize)
}
impl RenderTty for OutputHandle {
fn get_size_in_cells(&mut self) -> anyhow::Result<(usize, usize)> {
let info = self.get_buffer_info()?;
let (cols, rows) = dimensions_from_buffer_info(info);
Ok((cols, rows))
}
}
struct EventHandle {
handle: OwnedHandle,
}
@ -357,7 +384,7 @@ pub struct WindowsTerminal {
waker_handle: Arc<EventHandle>,
saved_input_mode: u32,
saved_output_mode: u32,
renderer: WindowsConsoleRenderer,
renderer: Renderer,
input_parser: InputParser,
input_queue: VecDeque<InputEvent>,
}
@ -403,7 +430,28 @@ impl WindowsTerminal {
let saved_input_mode = input_handle.get_input_mode()?;
let saved_output_mode = output_handle.get_output_mode()?;
let renderer = WindowsConsoleRenderer::new(caps);
/// Return true if the TERM environment is set to a string
/// that matches our builtin terminfo database for modern
/// windows 10/xterm compatible terminals.
fn term_is_builtin() -> bool {
if let Ok(t) = std::env::var("TERM") {
t == "xterm-256color"
} else {
false
}
}
let renderer = if caps.terminfo_db().is_some() {
Renderer::Terminfo(TerminfoRenderer::new(caps))
} else if term_is_builtin() {
// TODO: I'd like to automatically trigger this case if we're
// running on Windows 10 >= 1903, but let's hold off until we've
// had a bit more exposure with the TERM env based solution
Renderer::Terminfo(TerminfoRenderer::new(caps.apply_builtin_terminfo()))
} else {
Renderer::Windows(WindowsConsoleRenderer::new(caps))
};
let input_parser = InputParser::new();
Ok(Self {
@ -487,18 +535,11 @@ impl Terminal for WindowsTerminal {
fn get_screen_size(&mut self) -> Result<ScreenSize, Error> {
let info = self.output_handle.get_buffer_info()?;
// NOTE: the default console behavior is different from unix style
// terminals wrt. handling printing in the last column position.
// We under report the width by one to make it easier to have similar
// semantics to unix style terminals.
let visible_width = 0 + (info.srWindow.Right - info.srWindow.Left);
let visible_height = 1 + (info.srWindow.Bottom - info.srWindow.Top);
let (cols, rows) = dimensions_from_buffer_info(info);
Ok(ScreenSize {
rows: cast(visible_height)?,
cols: cast(visible_width)?,
rows: cast(rows)?,
cols: cast(cols)?,
xpixel: 0,
ypixel: 0,
})
@ -523,9 +564,12 @@ impl Terminal for WindowsTerminal {
}
fn render(&mut self, changes: &[Change]) -> Result<(), Error> {
self.renderer
.render_to(changes, &mut self.input_handle, &mut self.output_handle)
match &mut self.renderer {
Renderer::Terminfo(r) => r.render_to(changes, &mut self.output_handle),
Renderer::Windows(r) => r.render_to(changes, &mut self.output_handle),
}
}
fn flush(&mut self) -> Result<(), Error> {
self.output_handle
.flush()