T18 Terminal: a real shell on a pseudo-terminal through alacritty_terminal 0.26, drawn cell by cell in the theme's ANSI colors, with keys, text input, selection, scrollback, links and regex find; replayed output; tabs, splits and toolbar with a shell selector; command blocks and history; AnsiText, LogViewer and ProcessList
This commit is contained in:
1 parent
dd09d2802a
commit
7da1368cd9
30 files changed
+3527
-12
No files matched your search
@@ -0,0 +1,134 @@
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
|
||||
use alacritty_terminal::{
|
||||
event::{Event, EventListener, WindowSize},
|
||||
event_loop::{EventLoop, Notifier},
|
||||
sync::FairMutex,
|
||||
term::{Config, Term, test::TermSize},
|
||||
tty::{self, Options, Shell},
|
||||
};
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
|
||||
|
||||
/// Hands a terminal's events to its view.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Listener(UnboundedSender<Event>);
|
||||
|
||||
impl EventListener for Listener {
|
||||
fn send_event(&self, event: Event) {
|
||||
if self.0.unbounded_send(event).is_err() {
|
||||
log::debug!("terminal: an event after its view closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a terminal runs: a program and its arguments, the login shell when none; where; and variables on top of TERM, COLORTERM, and LANG when the app has none.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Launch {
|
||||
pub program: Option<(String, Vec<String>)>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// A grid, the channel of its events, and the pseudo-terminal that feeds it when one does.
|
||||
pub(crate) struct Session {
|
||||
pub term: Arc<FairMutex<Term<Listener>>>,
|
||||
pub pty: Option<Notifier>,
|
||||
pub events: UnboundedReceiver<Event>,
|
||||
}
|
||||
|
||||
/// A grid no process feeds, for replayed output.
|
||||
pub(crate) fn detached(size: TermSize) -> Session {
|
||||
let (sender, events) = unbounded();
|
||||
let term = Term::new(Config::default(), &size, Listener(sender));
|
||||
Session {
|
||||
term: Arc::new(FairMutex::new(term)),
|
||||
pty: None,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts `launch` on a pseudo-terminal of `size` cells, each `cell` pixels.
|
||||
pub(crate) fn spawn(launch: &Launch, size: TermSize, cell: (u16, u16)) -> anyhow::Result<Session> {
|
||||
let (sender, events) = unbounded();
|
||||
let listener = Listener(sender);
|
||||
let term = Arc::new(FairMutex::new(Term::new(
|
||||
Config::default(),
|
||||
&size,
|
||||
listener.clone(),
|
||||
)));
|
||||
let mut env: HashMap<String, String> = HashMap::from([
|
||||
("TERM".to_string(), "xterm-256color".to_string()),
|
||||
("COLORTERM".to_string(), "truecolor".to_string()),
|
||||
]);
|
||||
if std::env::var_os("LANG").is_none() {
|
||||
env.insert("LANG".into(), "en_US.UTF-8".into());
|
||||
}
|
||||
env.extend(launch.env.iter().cloned());
|
||||
let options = Options {
|
||||
shell: launch
|
||||
.program
|
||||
.clone()
|
||||
.map(|(program, args)| Shell::new(program, args)),
|
||||
working_directory: launch.cwd.clone(),
|
||||
drain_on_exit: true,
|
||||
env,
|
||||
};
|
||||
let pty = tty::new(&options, window_size(&size, cell), 0)?;
|
||||
let event_loop = EventLoop::new(term.clone(), listener, pty, true, false)?;
|
||||
let pty = Notifier(event_loop.channel());
|
||||
event_loop.spawn();
|
||||
log::info!("terminal: started {:?}", launch.program);
|
||||
Ok(Session {
|
||||
term,
|
||||
pty: Some(pty),
|
||||
events,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn window_size(size: &TermSize, cell: (u16, u16)) -> WindowSize {
|
||||
let cells =
|
||||
|count: usize| u16::try_from(count).expect("a terminal is under 65536 cells a side");
|
||||
WindowSize {
|
||||
num_lines: cells(size.screen_lines),
|
||||
num_cols: cells(size.columns),
|
||||
cell_width: cell.0,
|
||||
cell_height: cell.1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use alacritty_terminal::index::{Column, Line};
|
||||
use futures::StreamExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_program_prints_into_the_grid_through_a_pseudo_terminal() {
|
||||
let launch = Launch {
|
||||
program: Some((
|
||||
"/bin/sh".into(),
|
||||
vec!["-c".into(), "printf 'ely %s' \"$ELY\"".into()],
|
||||
)),
|
||||
env: vec![("ELY".into(), "ok".into())],
|
||||
..Launch::default()
|
||||
};
|
||||
let mut session = spawn(&launch, TermSize::new(20, 4), (8, 16)).expect("sh starts");
|
||||
let status = futures::executor::block_on(async {
|
||||
let mut status = None;
|
||||
while let Some(event) = session.events.next().await {
|
||||
match event {
|
||||
Event::ChildExit(ended) => status = Some(ended),
|
||||
Event::Exit => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
status
|
||||
});
|
||||
assert!(status.expect("the program ended").success());
|
||||
let term = session.term.lock();
|
||||
let row = &term.grid()[Line(0)];
|
||||
let text: String = (0..6).map(|column| row[Column(column)].c).collect();
|
||||
assert_eq!(text, "ely ok");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user