Initial commit: VT220 emulator with multi-window support

This commit is contained in:
root
2026-05-18 07:20:04 +00:00
commit 88daf1a5b6
7 changed files with 4162 additions and 0 deletions

504
src/main.rs Normal file
View File

@@ -0,0 +1,504 @@
mod renderer;
mod terminal;
use copypasta::{ClipboardContext, ClipboardProvider};
use renderer::{CellColor, CursorInfo, TerminalLine, TerminalRenderer, TextSpan};
use std::collections::HashMap;
use std::sync::Arc;
use terminal::TerminalEmulator;
use winit::{
application::ApplicationHandler,
dpi::LogicalSize,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
keyboard::{Key, NamedKey, ModifiersState},
window::{Window, WindowId},
};
fn main() {
env_logger::init();
let event_loop = EventLoop::new().unwrap();
let mut app = App {
windows: HashMap::new(),
next_window_offset: (50, 50),
};
event_loop.run_app(&mut app).unwrap();
}
struct App {
windows: HashMap<WindowId, WindowState>,
next_window_offset: (i32, i32),
}
struct WindowState {
window: Arc<Window>,
renderer: TerminalRenderer,
terminal: TerminalEmulator,
clipboard: Option<ClipboardContext>,
columns: usize,
lines: usize,
needs_redraw: bool,
modifiers: ModifiersState,
}
impl App {
fn create_window(&mut self, event_loop: &ActiveEventLoop) {
let (ox, oy) = self.next_window_offset;
self.next_window_offset = ((ox + 30) % 300, (oy + 30) % 300);
let window_attributes = Window::default_attributes()
.with_inner_size(LogicalSize::new(800.0, 600.0))
.with_title("VT220 Emulator")
.with_position(winit::dpi::PhysicalPosition::new(ox, oy));
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
let renderer = pollster::block_on(TerminalRenderer::new(window.clone(), event_loop));
let inner = window.inner_size();
let cols = (inner.width as f32 / renderer.cell_width).max(2.0) as usize;
let lines = (inner.height as f32 / renderer.cell_height).max(1.0) as usize;
let terminal = TerminalEmulator::new(cols, lines).expect("Failed to create terminal");
let clipboard = ClipboardContext::new().ok();
let window_id = window.id();
let state = WindowState {
window,
renderer,
terminal,
clipboard,
columns: cols,
lines,
needs_redraw: true,
modifiers: ModifiersState::empty(),
};
self.windows.insert(window_id, state);
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.windows.is_empty() {
self.create_window(event_loop);
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
// Handle window destruction events first to avoid borrow issues.
match event {
WindowEvent::CloseRequested | WindowEvent::Destroyed => {
self.windows.remove(&window_id);
if self.windows.is_empty() {
event_loop.exit();
}
return;
}
_ => {}
}
let Some(state) = self.windows.get_mut(&window_id) else {
return;
};
match event {
WindowEvent::Resized(size) => {
state.renderer.resize(size);
let cols = (size.width as f32 / state.renderer.cell_width).max(2.0) as usize;
let lines = (size.height as f32 / state.renderer.cell_height).max(1.0) as usize;
state.columns = cols;
state.lines = lines;
state.terminal.resize(
cols,
lines,
state.renderer.cell_width as u16,
state.renderer.cell_height as u16,
);
state.window.request_redraw();
}
WindowEvent::KeyboardInput { event, .. } => {
let logical_key = event.logical_key;
let mods = state.modifiers;
// New window shortcut.
if mods.control_key() && mods.shift_key() {
if let Key::Character(c) = &logical_key {
if c.as_str().eq_ignore_ascii_case("n") {
self.create_window(event_loop);
return;
}
}
}
// Copy / Paste shortcuts.
if mods.control_key() && mods.shift_key() {
if let Key::Character(c) = &logical_key {
let ch = c.as_str();
if ch.eq_ignore_ascii_case("c") {
let clipboard = &mut state.clipboard;
state.terminal.with_term(|term| {
if let Some(text) = term.selection_to_string() {
if let Some(cb) = clipboard {
let _ = cb.set_contents(text);
}
}
});
return;
}
if ch.eq_ignore_ascii_case("v") {
if let Some(cb) = &mut state.clipboard {
if let Ok(text) = cb.get_contents() {
state.terminal.send_input(text.as_bytes());
}
}
return;
}
}
}
let mut input_bytes: Vec<u8> = Vec::new();
match logical_key {
Key::Named(NamedKey::Enter) => {
input_bytes.push(b'\r');
}
Key::Named(NamedKey::Backspace) => {
input_bytes.push(0x7f);
}
Key::Named(NamedKey::Tab) => {
input_bytes.push(b'\t');
}
Key::Named(NamedKey::Escape) => {
input_bytes.push(0x1b);
}
Key::Named(NamedKey::ArrowUp) => {
input_bytes.extend_from_slice(b"\x1b[A");
}
Key::Named(NamedKey::ArrowDown) => {
input_bytes.extend_from_slice(b"\x1b[B");
}
Key::Named(NamedKey::ArrowRight) => {
input_bytes.extend_from_slice(b"\x1b[C");
}
Key::Named(NamedKey::ArrowLeft) => {
input_bytes.extend_from_slice(b"\x1b[D");
}
Key::Named(NamedKey::Home) => {
input_bytes.extend_from_slice(b"\x1b[H");
}
Key::Named(NamedKey::End) => {
input_bytes.extend_from_slice(b"\x1b[F");
}
Key::Named(NamedKey::Delete) => {
input_bytes.extend_from_slice(b"\x1b[3~");
}
Key::Named(NamedKey::Insert) => {
input_bytes.extend_from_slice(b"\x1b[2~");
}
Key::Named(NamedKey::PageUp) => {
input_bytes.extend_from_slice(b"\x1b[5~");
}
Key::Named(NamedKey::PageDown) => {
input_bytes.extend_from_slice(b"\x1b[6~");
}
Key::Named(NamedKey::F1) => {
input_bytes.extend_from_slice(b"\x1bOP");
}
Key::Named(NamedKey::F2) => {
input_bytes.extend_from_slice(b"\x1bOQ");
}
Key::Named(NamedKey::F3) => {
input_bytes.extend_from_slice(b"\x1bOR");
}
Key::Named(NamedKey::F4) => {
input_bytes.extend_from_slice(b"\x1bOS");
}
Key::Character(c) => {
let s = c.as_str();
if s.len() == 1 {
let b = s.as_bytes()[0];
input_bytes.push(b);
} else {
input_bytes.extend_from_slice(s.as_bytes());
}
}
_ => {}
}
if !input_bytes.is_empty() {
state.terminal.send_input(&input_bytes);
state.window.request_redraw();
}
}
WindowEvent::ModifiersChanged(mods) => {
state.modifiers = mods.state();
}
WindowEvent::MouseWheel { delta, .. } => {
let lines = match delta {
winit::event::MouseScrollDelta::LineDelta(_, y) => -y as i32,
winit::event::MouseScrollDelta::PixelDelta(pos) => {
-(pos.y / state.renderer.cell_height as f64) as i32
}
};
if lines != 0 {
state.terminal.with_term(|term| {
term.scroll_display(alacritty_terminal::grid::Scroll::Delta(lines));
});
state.window.request_redraw();
}
}
WindowEvent::RedrawRequested => {
state.needs_redraw = true;
}
_ => {}
}
if state.needs_redraw {
state.needs_redraw = false;
draw(state);
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
let mut windows_to_redraw = Vec::new();
for (window_id, state) in &mut self.windows {
let events = state.terminal.read_events();
let mut need_redraw = false;
for event in events {
match event {
alacritty_terminal::event::Event::Wakeup => {
need_redraw = true;
}
alacritty_terminal::event::Event::ClipboardStore(_ty, text) => {
if let Some(cb) = &mut state.clipboard {
let _ = cb.set_contents(text);
}
}
alacritty_terminal::event::Event::ClipboardLoad(_ty, formatter) => {
if let Some(cb) = &mut state.clipboard {
if let Ok(content) = cb.get_contents() {
let formatted = formatter(&content);
state.terminal.send_input(formatted.as_bytes());
}
}
}
alacritty_terminal::event::Event::Title(title) => {
state.window.set_title(&format!("VT220 Emulator - {}", title));
}
alacritty_terminal::event::Event::Exit => {
windows_to_redraw.push(*window_id);
}
_ => {}
}
}
if need_redraw {
state.window.request_redraw();
}
}
for window_id in windows_to_redraw {
self.windows.remove(&window_id);
}
if self.windows.is_empty() {
_event_loop.exit();
}
}
}
fn draw(state: &mut WindowState) {
let mut lines: Vec<TerminalLine> = Vec::with_capacity(state.lines);
let mut cursor: Option<CursorInfo> = None;
state.terminal.with_term(|term| {
let grid = term.grid();
let colors = term.colors();
let mode = *term.mode();
let display_offset = grid.display_offset() as i32;
let mut current_line_spans: Vec<TextSpan> = Vec::new();
let mut current_text = String::new();
let mut current_fg = CellColor::new(255, 255, 255);
let mut last_line = 0i32;
let cursor_point = grid.cursor.point;
let cursor_viewport_y = cursor_point.line.0 + display_offset;
let cursor_info = CursorInfo {
x: cursor_point.column.0,
y: cursor_viewport_y as usize,
visible: mode.contains(alacritty_terminal::term::TermMode::SHOW_CURSOR),
};
cursor = Some(cursor_info);
for indexed in grid.display_iter() {
let cell = indexed.cell;
let point = indexed.point;
let line_in_viewport = point.line.0 + display_offset;
if line_in_viewport != last_line {
if !current_text.is_empty() {
current_line_spans.push(TextSpan {
text: current_text.clone(),
fg: current_fg,
});
}
lines.push(TerminalLine {
spans: std::mem::take(&mut current_line_spans),
});
current_text.clear();
last_line = line_in_viewport;
}
let fg = resolve_color(cell.fg, colors, true);
let bg = resolve_color(cell.bg, colors, false);
let is_cursor = cursor_info.visible
&& cursor_info.x == point.column.0
&& cursor_info.y == line_in_viewport as usize;
if is_cursor {
if !current_text.is_empty() {
current_line_spans.push(TextSpan {
text: current_text.clone(),
fg: current_fg,
});
current_text.clear();
}
let ch = if cell.c == '\0' { ' ' } else { cell.c };
current_line_spans.push(TextSpan {
text: ch.to_string(),
fg: bg,
});
current_fg = fg;
} else {
if fg != current_fg && !current_text.is_empty() {
current_line_spans.push(TextSpan {
text: current_text.clone(),
fg: current_fg,
});
current_text.clear();
}
current_fg = fg;
let ch = if cell.c == '\0' { ' ' } else { cell.c };
current_text.push(ch);
if let Some(zw) = cell.zerowidth() {
for &z in zw {
current_text.push(z);
}
}
}
}
if !current_text.is_empty() {
current_line_spans.push(TextSpan {
text: current_text,
fg: current_fg,
});
}
if !current_line_spans.is_empty() {
lines.push(TerminalLine {
spans: current_line_spans,
});
}
while lines.len() < state.lines {
lines.push(TerminalLine { spans: vec![] });
}
});
state.renderer.render(&lines, cursor);
}
fn resolve_color(
color: alacritty_terminal::vte::ansi::Color,
colors: &alacritty_terminal::term::color::Colors,
is_fg: bool,
) -> CellColor {
use alacritty_terminal::vte::ansi::Color;
let rgb = match color {
Color::Named(named) => {
if let Some(rgb) = colors[named] {
rgb
} else {
return named_color_default(named, is_fg);
}
}
Color::Spec(rgb) => rgb,
Color::Indexed(idx) => {
if let Some(rgb) = colors[idx as usize] {
rgb
} else {
return indexed_color_default(idx);
}
}
};
CellColor::new(rgb.r, rgb.g, rgb.b)
}
fn named_color_default(
named: alacritty_terminal::vte::ansi::NamedColor,
is_fg: bool,
) -> CellColor {
use alacritty_terminal::vte::ansi::NamedColor;
match named {
NamedColor::Black => CellColor::new(0, 0, 0),
NamedColor::Red => CellColor::new(205, 0, 0),
NamedColor::Green => CellColor::new(0, 205, 0),
NamedColor::Yellow => CellColor::new(205, 205, 0),
NamedColor::Blue => CellColor::new(0, 0, 238),
NamedColor::Magenta => CellColor::new(205, 0, 205),
NamedColor::Cyan => CellColor::new(0, 205, 205),
NamedColor::White => CellColor::new(229, 229, 229),
NamedColor::BrightBlack => CellColor::new(127, 127, 127),
NamedColor::BrightRed => CellColor::new(255, 0, 0),
NamedColor::BrightGreen => CellColor::new(0, 255, 0),
NamedColor::BrightYellow => CellColor::new(255, 255, 0),
NamedColor::BrightBlue => CellColor::new(92, 92, 255),
NamedColor::BrightMagenta => CellColor::new(255, 0, 255),
NamedColor::BrightCyan => CellColor::new(0, 255, 255),
NamedColor::BrightWhite => CellColor::new(255, 255, 255),
NamedColor::Foreground => {
if is_fg {
CellColor::new(229, 229, 229)
} else {
CellColor::new(0, 0, 0)
}
}
NamedColor::Background => {
if is_fg {
CellColor::new(229, 229, 229)
} else {
CellColor::new(0, 0, 0)
}
}
_ => {
if is_fg {
CellColor::new(229, 229, 229)
} else {
CellColor::new(0, 0, 0)
}
}
}
}
fn indexed_color_default(idx: u8) -> CellColor {
if idx < 16 {
return CellColor::new(128, 128, 128);
} else if idx < 232 {
let i = idx - 16;
let r = (i / 36) * 51;
let g = ((i % 36) / 6) * 51;
let b = (i % 6) * 51;
CellColor::new(r, g, b)
} else {
let gray = (idx - 232) * 10 + 8;
CellColor::new(gray, gray, gray)
}
}

305
src/renderer.rs Normal file
View File

@@ -0,0 +1,305 @@
use glyphon::{
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
};
use std::sync::Arc;
use wgpu::{
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
InstanceDescriptor, LoadOp, MultisampleState, Operations, PresentMode, Queue,
RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, Surface,
SurfaceConfiguration, TextureFormat, TextureUsages, TextureViewDescriptor,
};
use winit::{
dpi::PhysicalSize,
event_loop::ActiveEventLoop,
window::Window,
};
pub const FONT_SIZE: f32 = 14.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CellColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl CellColor {
pub fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
pub struct TextSpan {
pub text: String,
pub fg: CellColor,
}
pub struct TerminalLine {
pub spans: Vec<TextSpan>,
}
#[derive(Clone, Copy)]
pub struct CursorInfo {
pub x: usize,
pub y: usize,
pub visible: bool,
}
pub struct TerminalRenderer {
pub device: Device,
pub queue: Queue,
pub surface: Surface<'static>,
pub surface_config: SurfaceConfiguration,
pub instance: Instance,
pub font_system: FontSystem,
pub swash_cache: SwashCache,
pub viewport: Viewport,
pub atlas: TextAtlas,
pub text_renderer: TextRenderer,
pub line_buffers: Vec<Buffer>,
pub cell_width: f32,
pub cell_height: f32,
pub _metrics: Metrics,
pub window: Arc<Window>,
}
impl TerminalRenderer {
pub async fn new(window: Arc<Window>, event_loop: &ActiveEventLoop) -> Self {
let physical_size = window.inner_size();
let scale_factor = window.scale_factor() as f32;
let instance = Instance::new(InstanceDescriptor::new_with_display_handle(Box::new(
event_loop.owned_display_handle(),
)));
let adapter = instance
.request_adapter(&RequestAdapterOptions::default())
.await
.unwrap();
let (device, queue) = adapter
.request_device(&DeviceDescriptor::default())
.await
.unwrap();
let surface = instance
.create_surface(window.clone())
.expect("Create surface");
let swapchain_format = TextureFormat::Bgra8UnormSrgb;
let surface_config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: physical_size.width,
height: physical_size.height,
present_mode: PresentMode::Fifo,
alpha_mode: CompositeAlphaMode::Opaque,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
let mut font_system = FontSystem::new();
let swash_cache = SwashCache::new();
let cache = Cache::new(&device);
let viewport = Viewport::new(&device, &cache);
let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format);
let text_renderer =
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
let metrics = Metrics::new(FONT_SIZE * scale_factor, FONT_SIZE * 1.4 * scale_factor);
let mut measure_buffer = Buffer::new(&mut font_system, metrics);
measure_buffer.set_size(&mut font_system, Some(1000.0), Some(1000.0));
measure_buffer.set_text(
&mut font_system,
"M",
&Attrs::new().family(Family::Monospace),
Shaping::Basic,
None,
);
measure_buffer.shape_until_scroll(&mut font_system, false);
let cell_width = measure_buffer
.lines
.first()
.and_then(|l| l.layout_opt())
.and_then(|layouts| layouts.first())
.map(|layout| layout.w)
.unwrap_or(FONT_SIZE * scale_factor * 0.6);
let cell_height = metrics.line_height;
let max_lines = 200;
let mut line_buffers = Vec::with_capacity(max_lines);
for _ in 0..max_lines {
line_buffers.push(Buffer::new(&mut font_system, metrics));
}
Self {
device,
queue,
surface,
surface_config,
instance,
font_system,
swash_cache,
viewport,
atlas,
text_renderer,
line_buffers,
cell_width,
cell_height,
_metrics: metrics,
window,
}
}
pub fn resize(&mut self, size: PhysicalSize<u32>) {
self.surface_config.width = size.width;
self.surface_config.height = size.height;
self.surface.configure(&self.device, &self.surface_config);
}
pub fn render(
&mut self,
lines: &[TerminalLine],
cursor: Option<CursorInfo>,
) {
let width = self.surface_config.width;
let height = self.surface_config.height;
self.viewport.update(
&self.queue,
Resolution { width, height },
);
let scale_factor = self.window.scale_factor() as f32;
let buf_width = width as f32 * scale_factor;
// Phase 1: Update all line buffers.
for (line_idx, line) in lines.iter().enumerate() {
if line_idx >= self.line_buffers.len() {
break;
}
let buffer = &mut self.line_buffers[line_idx];
buffer.set_size(&mut self.font_system, Some(buf_width), Some(self.cell_height));
if line.spans.is_empty() {
buffer.set_text(&mut self.font_system, " ", &Attrs::new(), Shaping::Basic, None);
} else {
let spans_iter = line.spans.iter().map(|span| {
let color = Color::rgb(span.fg.r, span.fg.g, span.fg.b);
let attrs = Attrs::new().family(Family::Monospace).color(color);
(span.text.as_str(), attrs)
});
buffer.set_rich_text(&mut self.font_system, spans_iter, &Attrs::new(), Shaping::Basic, None);
}
buffer.shape_until_scroll(&mut self.font_system, false);
}
// Phase 2: Build text areas.
let text_areas: Vec<TextArea> = self
.line_buffers
.iter()
.enumerate()
.take(lines.len())
.map(|(line_idx, buffer)| {
let top = line_idx as f32 * self.cell_height;
TextArea {
buffer,
left: 0.0,
top,
scale: 1.0,
bounds: TextBounds {
left: 0,
top: 0,
right: width as i32,
bottom: height as i32,
},
default_color: Color::rgb(255, 255, 255),
custom_glyphs: &[],
}
})
.collect();
// Phase 3: Prepare.
if let Err(e) = self.text_renderer.prepare(
&self.device,
&self.queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
text_areas,
&mut self.swash_cache,
) {
log::warn!("Text prepare error: {:?}", e);
return;
}
let frame = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
wgpu::CurrentSurfaceTexture::Timeout
| wgpu::CurrentSurfaceTexture::Occluded => {
self.window.request_redraw();
return;
}
wgpu::CurrentSurfaceTexture::Outdated
| wgpu::CurrentSurfaceTexture::Suboptimal(_) => {
self.surface.configure(&self.device, &self.surface_config);
self.window.request_redraw();
return;
}
wgpu::CurrentSurfaceTexture::Lost => {
self.surface = self.instance.create_surface(self.window.clone()).unwrap();
self.surface.configure(&self.device, &self.surface_config);
self.window.request_redraw();
return;
}
wgpu::CurrentSurfaceTexture::Validation => panic!("validation error"),
};
let view = frame.texture.create_view(&TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { label: None });
{
let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(wgpu::Color {
r: 0.05,
g: 0.05,
b: 0.05,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if let Some(cursor) = cursor {
if cursor.visible && cursor.y < lines.len() {
let _x = cursor.x as f32 * self.cell_width;
let _y = cursor.y as f32 * self.cell_height;
// TODO: draw cursor quad.
}
}
if let Err(e) = self.text_renderer.render(&self.atlas, &self.viewport, &mut pass) {
log::warn!("Text render error: {:?}", e);
}
}
self.queue.submit(Some(encoder.finish()));
frame.present();
self.atlas.trim();
}
}

128
src/terminal.rs Normal file
View File

@@ -0,0 +1,128 @@
use alacritty_terminal::{
event::{Event, EventListener, WindowSize},
event_loop::{EventLoop, Msg},
sync::FairMutex,
term::{Config, Term},
tty::{self, Options},
};
use std::{
io,
sync::{Arc, mpsc},
};
/// Proxy that forwards alacritty_terminal events into an MPSC channel.
#[derive(Clone)]
pub struct EventProxy {
sender: mpsc::Sender<Event>,
}
impl EventListener for EventProxy {
fn send_event(&self, event: Event) {
let _ = self.sender.send(event);
}
}
/// High-level wrapper around alacritty_terminal.
pub struct TerminalEmulator {
terminal: Arc<FairMutex<Term<EventProxy>>>,
event_sender: alacritty_terminal::event_loop::EventLoopSender,
event_receiver: mpsc::Receiver<Event>,
}
impl TerminalEmulator {
pub fn new(columns: usize, lines: usize) -> io::Result<Self> {
let (event_sender, event_receiver) = mpsc::channel();
let proxy = EventProxy {
sender: event_sender,
};
let size = TermSize {
columns,
screen_lines: lines,
};
let term = Term::new(Config::default(), &size, proxy.clone());
let terminal = Arc::new(FairMutex::new(term));
let window_size = WindowSize {
num_lines: lines as u16,
num_cols: columns as u16,
cell_width: 10,
cell_height: 20,
};
let pty = tty::new(&Options::default(), window_size, 0)?;
let event_loop = EventLoop::new(terminal.clone(), proxy, pty, false, false)?;
let tx = event_loop.channel();
// Spawn the PTY reader thread.
let _handle = event_loop.spawn();
Ok(Self {
terminal,
event_sender: tx,
event_receiver,
})
}
pub fn resize(&self, columns: usize, lines: usize, cell_width: u16, cell_height: u16) {
let window_size = WindowSize {
num_lines: lines as u16,
num_cols: columns as u16,
cell_width,
cell_height,
};
self.terminal.lock().resize(TermSize { columns, screen_lines: lines });
let _ = self.event_sender.send(Msg::Resize(window_size));
}
pub fn send_input(&self, data: &[u8]) {
let _ = self
.event_sender
.send(Msg::Input(std::borrow::Cow::Owned(data.to_vec())));
}
pub fn read_events(&self) -> Vec<Event> {
let mut events = Vec::new();
while let Ok(event) = self.event_receiver.try_recv() {
events.push(event);
}
events
}
pub fn with_term<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut Term<EventProxy>) -> R,
{
let mut term = self.terminal.lock();
f(&mut term)
}
}
impl Drop for TerminalEmulator {
fn drop(&mut self) {
let _ = self.event_sender.send(Msg::Shutdown);
}
}
#[derive(Clone, Copy)]
pub struct TermSize {
pub columns: usize,
pub screen_lines: usize,
}
impl alacritty_terminal::grid::Dimensions for TermSize {
fn total_lines(&self) -> usize {
self.screen_lines
}
fn screen_lines(&self) -> usize {
self.screen_lines
}
fn columns(&self) -> usize {
self.columns
}
}