268 lines
8.1 KiB
Rust
268 lines
8.1 KiB
Rust
// Copyright (c) 2022 Marceline Cramer
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use alacritty_terminal::ansi::Color;
|
|
use alacritty_terminal::config::PtyConfig;
|
|
use alacritty_terminal::event::{Event as TermEvent, EventListener};
|
|
use alacritty_terminal::event_loop::{
|
|
EventLoop as TermEventLoop, Msg as TermMsg, State as TermState,
|
|
};
|
|
use alacritty_terminal::sync::FairMutex;
|
|
use alacritty_terminal::term::color::Rgb;
|
|
use alacritty_terminal::tty::Pty;
|
|
use alacritty_terminal::Term;
|
|
use mio_extras::channel::Sender as MioSender;
|
|
use softbuffer::GraphicsContext;
|
|
use std::borrow::Cow;
|
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
|
use std::sync::Arc;
|
|
use std::thread::JoinHandle;
|
|
use winit::event::{Event, WindowEvent};
|
|
use winit::event_loop::{ControlFlow, EventLoop};
|
|
use winit::window::{Window, WindowBuilder, WindowId};
|
|
|
|
pub mod config;
|
|
pub mod graphics;
|
|
|
|
use config::Config;
|
|
use graphics::Graphics;
|
|
|
|
pub struct TermListener {
|
|
sender: Sender<TermEvent>,
|
|
}
|
|
|
|
impl TermListener {
|
|
pub fn new(sender: Sender<TermEvent>) -> Self {
|
|
Self { sender }
|
|
}
|
|
}
|
|
|
|
impl EventListener for TermListener {
|
|
fn send_event(&self, event: TermEvent) {
|
|
self.sender.send(event).unwrap();
|
|
}
|
|
}
|
|
|
|
fn get_login_shell(config: Config) -> String {
|
|
if config.system.shell != "" {
|
|
config.system.shell
|
|
} else {
|
|
match std::env::consts::OS {
|
|
"linux" | "openbsd" | "netbsd" | "dragonfly" | "solaris" | "macos" => {
|
|
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
|
|
}
|
|
"windows" => r#"C:\Windows\System32\cmd.exe"#.to_string(),
|
|
_ => unimplemented!("Unrecognized operating system; cannot get user's shell"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct App {
|
|
graphics: Graphics,
|
|
graphics_context: GraphicsContext<Window>,
|
|
should_quit: bool,
|
|
term_loop: JoinHandle<(TermEventLoop<Pty, TermListener>, TermState)>,
|
|
term_channel: MioSender<TermMsg>,
|
|
term_events: Receiver<TermEvent>,
|
|
term: Arc<FairMutex<Term<TermListener>>>,
|
|
}
|
|
|
|
impl App {
|
|
pub fn new(config: Config, window: Window) -> Self {
|
|
let graphics = Graphics::new(&config);
|
|
|
|
let term_size = alacritty_terminal::term::SizeInfo::new(
|
|
graphics.canvas.width as f32,
|
|
graphics.canvas.height as f32,
|
|
graphics.cell_width as f32,
|
|
graphics.cell_height as f32,
|
|
graphics.pad_x as f32,
|
|
graphics.pad_y as f32,
|
|
false,
|
|
);
|
|
|
|
let (sender, term_events) = channel();
|
|
|
|
let term_config = alacritty_terminal::config::Config {
|
|
pty_config: PtyConfig {
|
|
shell: Some(alacritty_terminal::config::Program::Just(get_login_shell(
|
|
config,
|
|
))),
|
|
working_directory: None,
|
|
hold: false,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
alacritty_terminal::tty::setup_env(&term_config);
|
|
|
|
let term_listener = TermListener::new(sender.clone());
|
|
|
|
let term = Term::new(&term_config, term_size, term_listener);
|
|
let term = FairMutex::new(term);
|
|
let term = Arc::new(term);
|
|
|
|
let pty = alacritty_terminal::tty::new(&term_config.pty_config, &term_size, None).unwrap();
|
|
|
|
let term_listener = TermListener::new(sender);
|
|
let term_loop = TermEventLoop::new(term.clone(), term_listener, pty, false, false);
|
|
let term_channel = term_loop.channel();
|
|
|
|
Self {
|
|
graphics,
|
|
graphics_context: unsafe { GraphicsContext::new(window).unwrap() },
|
|
should_quit: false,
|
|
term,
|
|
term_channel,
|
|
term_events,
|
|
term_loop: term_loop.spawn(),
|
|
}
|
|
}
|
|
|
|
pub fn should_quit(&self) -> bool {
|
|
self.should_quit
|
|
}
|
|
|
|
pub fn update(&mut self) {
|
|
while let Ok(event) = self.term_events.try_recv() {
|
|
match event {
|
|
TermEvent::ColorRequest(index, format) => {
|
|
let rgb = self.graphics.get_color_idx(index);
|
|
self.send_input(&format(rgb));
|
|
}
|
|
TermEvent::PtyWrite(text) => self.send_input(&text),
|
|
TermEvent::Wakeup => self.request_redraw(),
|
|
TermEvent::Exit => self.should_quit = true,
|
|
event => eprintln!("unhandled event: {:#?}", event),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn request_redraw(&self) {
|
|
self.graphics_context.window().request_redraw();
|
|
}
|
|
|
|
pub fn redraw(&mut self) {
|
|
let term = self.term.lock();
|
|
self.graphics.redraw(&term, &mut self.graphics_context);
|
|
}
|
|
|
|
pub fn virtual_keycode_to_string(
|
|
keycode: winit::event::VirtualKeyCode,
|
|
) -> Option<&'static str> {
|
|
use winit::event::VirtualKeyCode::*;
|
|
match keycode {
|
|
Back => Some("\x7f"),
|
|
Up => Some("\x1b[A"),
|
|
Down => Some("\x1b[B"),
|
|
Right => Some("\x1b[C"),
|
|
Left => Some("\x1b[D"),
|
|
Home => Some("\x1b[1~"),
|
|
Insert => Some("\x1b[2~"),
|
|
Delete => Some("\x1b[3~"),
|
|
End => Some("\x1b[4~"),
|
|
PageUp => Some("\x1b[5~"),
|
|
PageDown => Some("\x1b[6~"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn on_keyboard_input(&mut self, input: &winit::event::KeyboardInput) {
|
|
if input.state == winit::event::ElementState::Pressed {
|
|
if let Some(keycode) = input.virtual_keycode {
|
|
if let Some(input) = Self::virtual_keycode_to_string(keycode) {
|
|
self.send_input(input);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn on_received_character(&mut self, c: char) {
|
|
log::debug!("Received character: {:?}", c);
|
|
|
|
match c {
|
|
'\u{7f}' | '\u{8}' => {
|
|
// We use a special escape code for the delete and backspace keys.
|
|
log::debug!("Ignoring.");
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let string = c.to_string();
|
|
self.send_input(string.as_str());
|
|
}
|
|
|
|
pub fn send_input(&mut self, input: &str) {
|
|
let bytes = input.as_bytes();
|
|
let cow = Cow::Owned(bytes.to_owned().into());
|
|
self.term_channel.send(TermMsg::Input(cow)).unwrap();
|
|
}
|
|
|
|
pub fn on_resize(&mut self, width: u32, height: u32) {
|
|
let term_size = alacritty_terminal::term::SizeInfo::new(
|
|
width as f32,
|
|
height as f32,
|
|
self.graphics.cell_width as f32,
|
|
self.graphics.cell_height as f32,
|
|
self.graphics.pad_x as f32,
|
|
self.graphics.pad_y as f32,
|
|
false,
|
|
);
|
|
|
|
self.term.lock().resize(term_size);
|
|
|
|
self.term_channel.send(TermMsg::Resize(term_size)).unwrap();
|
|
|
|
self.request_redraw();
|
|
}
|
|
|
|
pub fn window_id(&self) -> WindowId {
|
|
self.graphics_context.window().id()
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let config: Config = confy::load("piss", Some("config")).unwrap();
|
|
|
|
env_logger::init();
|
|
|
|
let event_loop = EventLoop::new();
|
|
let window = WindowBuilder::new().build(&event_loop).unwrap();
|
|
let mut app = App::new(config, window);
|
|
|
|
event_loop.run(move |event, _, control_flow| {
|
|
*control_flow = ControlFlow::Poll;
|
|
|
|
app.update();
|
|
|
|
match event {
|
|
Event::RedrawRequested(window_id) if window_id == app.window_id() => {
|
|
app.redraw();
|
|
}
|
|
Event::WindowEvent { event, window_id } if window_id == app.window_id() => {
|
|
match event {
|
|
WindowEvent::CloseRequested => {
|
|
*control_flow = ControlFlow::Exit;
|
|
}
|
|
WindowEvent::KeyboardInput { input, .. } => {
|
|
app.on_keyboard_input(&input);
|
|
}
|
|
WindowEvent::ReceivedCharacter(c) => {
|
|
app.on_received_character(c);
|
|
}
|
|
WindowEvent::Resized(size) => {
|
|
app.on_resize(size.width, size.height);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if app.should_quit() {
|
|
*control_flow = ControlFlow::Exit;
|
|
}
|
|
});
|
|
}
|