view exec'd processes status/output

This commit is contained in:
rabite 2019-02-28 18:43:11 +01:00
parent fd67621dee
commit 06817602a8
13 changed files with 398 additions and 70 deletions

View File

@ -32,7 +32,9 @@ pub enum HError {
#[fail(display = "No widget found")] #[fail(display = "No widget found")]
NoWidgetError, NoWidgetError,
#[fail(display = "Path: {:?} not in this directory: {:?}", path, dir)] #[fail(display = "Path: {:?} not in this directory: {:?}", path, dir)]
WrongDirectoryError{ path: PathBuf, dir: PathBuf } WrongDirectoryError{ path: PathBuf, dir: PathBuf },
#[fail(display = "Widget finnished")]
PopupFinnished,
} }
impl From<std::io::Error> for HError { impl From<std::io::Error> for HError {

View File

@ -17,6 +17,7 @@ use crate::tabview::{TabView, Tabbable};
use crate::preview::WillBeWidget; use crate::preview::WillBeWidget;
use crate::fail::{HResult, HError}; use crate::fail::{HResult, HError};
use crate::window::{Events, send_event}; use crate::window::{Events, send_event};
use crate::proclist::ProcView;
@ -25,12 +26,17 @@ pub struct FileBrowser {
pub cwd: File, pub cwd: File,
watcher: INotifyWatcher, watcher: INotifyWatcher,
watches: Vec<PathBuf>, watches: Vec<PathBuf>,
dir_events: Arc<Mutex<Vec<DebouncedEvent>>> dir_events: Arc<Mutex<Vec<DebouncedEvent>>>,
proc_view: Arc<Mutex<ProcView>>,
} }
impl Tabbable for TabView<FileBrowser> { impl Tabbable for TabView<FileBrowser> {
fn new_tab(&mut self) { fn new_tab(&mut self) {
let tab = FileBrowser::new().unwrap(); let mut tab = FileBrowser::new().unwrap();
let proc_view = self.active_tab_().proc_view.clone();
tab.proc_view = proc_view;
self.push_widget(tab); self.push_widget(tab);
self.active += 1; self.active += 1;
} }
@ -71,7 +77,7 @@ impl Tabbable for TabView<FileBrowser> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.widgets[self.active].exec_cmd(tab_dirs).ok(); self.widgets[self.active].exec_cmd(tab_dirs).ok();
} }
_ => self.active_tab_mut().on_key(key) _ => { self.active_tab_mut().on_key(key).ok(); }
} }
} }
} }
@ -125,11 +131,15 @@ impl FileBrowser {
let watcher = INotifyWatcher::new(tx_watch, Duration::from_secs(2)).unwrap(); let watcher = INotifyWatcher::new(tx_watch, Duration::from_secs(2)).unwrap();
watch_dir(rx_watch, dir_events.clone()); watch_dir(rx_watch, dir_events.clone());
let mut proc_view = ProcView::new();
proc_view.set_coordinates(&coords);
Ok(FileBrowser { columns: miller, Ok(FileBrowser { columns: miller,
cwd: cwd, cwd: cwd,
watcher: watcher, watcher: watcher,
watches: vec![], watches: vec![],
dir_events: dir_events }) dir_events: dir_events,
proc_view: Arc::new(Mutex::new(proc_view)) })
} }
pub fn enter_dir(&mut self) -> HResult<()> { pub fn enter_dir(&mut self) -> HResult<()> {
@ -250,7 +260,7 @@ impl FileBrowser {
self.watches.push(left_dir.path); self.watches.push(left_dir.path);
} }
if let Some(preview_dir) = preview_dir { if let Some(preview_dir) = preview_dir {
if !watched_dirs.contains(&preview_dir) { if !watched_dirs.contains(&preview_dir) && preview_dir.is_dir() {
self.watcher.watch(&preview_dir, RecursiveMode::NonRecursive).unwrap(); self.watcher.watch(&preview_dir, RecursiveMode::NonRecursive).unwrap();
self.watches.push(preview_dir); self.watches.push(preview_dir);
} }
@ -380,21 +390,8 @@ impl FileBrowser {
cmd = cmd.replace(&tab_identifier, &tab_path); cmd = cmd.replace(&tab_identifier, &tab_path);
} }
let status = std::process::Command::new("sh") self.proc_view.lock()?.run_proc(&cmd)?;
.arg("-c")
.arg(&cmd)
.status();
let mut bufout = std::io::BufWriter::new(std::io::stdout());
write!(bufout, "{}{}",
termion::style::Reset,
termion::clear::All).unwrap();
match status {
Ok(status) => self.show_status(&format!("\"{}\" exited with {}",
cmd, status)),
Err(err) => self.show_status(&format!("Can't run this \"{}\": {}",
cmd, err)),
}
Ok(()) Ok(())
} }
} }
@ -404,7 +401,8 @@ impl Widget for FileBrowser {
&self.columns.coordinates &self.columns.coordinates
} }
fn set_coordinates(&mut self, coordinates: &Coordinates) { fn set_coordinates(&mut self, coordinates: &Coordinates) {
self.columns.coordinates = coordinates.clone(); self.columns.set_coordinates(coordinates);
self.proc_view.lock().unwrap().set_coordinates(coordinates);
self.refresh(); self.refresh();
} }
fn render_header(&self) -> String { fn render_header(&self) -> String {
@ -467,15 +465,20 @@ impl Widget for FileBrowser {
} }
} }
fn on_key(&mut self, key: Key) { fn on_key(&mut self, key: Key) -> HResult<()> {
match key { match key {
Key::Char('/') => { self.turbo_cd().ok(); }, Key::Char('/') => { self.turbo_cd().ok(); },
Key::Char('Q') => { self.quit_with_dir().ok(); }, Key::Char('Q') => { self.quit_with_dir().ok(); },
Key::Right | Key::Char('f') => { self.enter_dir().ok(); }, Key::Right | Key::Char('f') => { self.enter_dir().ok(); },
Key::Left | Key::Char('b') => { self.go_back().ok(); }, Key::Left | Key::Char('b') => { self.go_back().ok(); },
_ => self.columns.get_main_widget_mut().unwrap().on_key(key), Key::Char('w') => {
self.proc_view.lock()?.popup().ok();
}
,
_ => { self.columns.get_main_widget_mut()?.on_key(key).ok(); },
} }
self.update_preview().ok(); self.update_preview().ok();
Ok(())
} }
} }

View File

@ -2,6 +2,7 @@ use termion::event::{Event};
use crate::widget::Widget; use crate::widget::Widget;
use crate::coordinates::{Coordinates, Size, Position}; use crate::coordinates::{Coordinates, Size, Position};
use crate::fail::HResult;
#[derive(PartialEq)] #[derive(PartialEq)]
pub struct HBox<T: Widget> { pub struct HBox<T: Widget> {
@ -106,7 +107,8 @@ impl<T> Widget for HBox<T> where T: Widget + PartialEq {
self.coordinates = coordinates.clone(); self.coordinates = coordinates.clone();
self.refresh(); self.refresh();
} }
fn on_event(&mut self, event: Event) { fn on_event(&mut self, event: Event) -> HResult<()> {
self.widgets.last_mut().unwrap().on_event(event); self.widgets.last_mut()?.on_event(event).ok();
Ok(())
} }
} }

View File

@ -90,7 +90,7 @@ where
view view
} }
fn move_up(&mut self) { pub fn move_up(&mut self) {
if self.selection == 0 { if self.selection == 0 {
return; return;
} }
@ -102,11 +102,11 @@ where
self.selection -= 1; self.selection -= 1;
self.seeking = false; self.seeking = false;
} }
fn move_down(&mut self) { pub fn move_down(&mut self) {
let lines = self.lines; let lines = self.lines;
let y_size = self.coordinates.ysize() as usize; let y_size = self.coordinates.ysize() as usize;
if self.selection == lines - 1 { if self.lines == 0 || self.selection == lines - 1 {
return; return;
} }
@ -379,7 +379,7 @@ impl<T> Widget for ListView<T> where ListView<T>: Listable {
fn refresh(&mut self) { fn refresh(&mut self) {
self.on_refresh(); self.on_refresh();
self.lines = self.len(); self.lines = self.len();
if self.selection >= self.lines { if self.selection >= self.lines && self.selection != 0 {
self.selection -= 1; self.selection -= 1;
} }
self.buffer = self.render(); self.buffer = self.render();
@ -418,7 +418,8 @@ impl<T> Widget for ListView<T> where ListView<T>: Listable {
format!("{} files", self.len()) format!("{} files", self.len())
} }
fn on_key(&mut self, key: Key) { fn on_key(&mut self, key: Key) -> HResult<()> {
Listable::on_key(self, key); Listable::on_key(self, key);
Ok(())
} }
} }

View File

@ -40,6 +40,9 @@ mod tabview;
mod async_widget; mod async_widget;
mod fail; mod fail;
mod minibuffer; mod minibuffer;
mod proclist;
use window::Window; use window::Window;

View File

@ -155,7 +155,8 @@ where
} }
} }
fn on_key(&mut self, key: Key) { fn on_key(&mut self, key: Key) -> HResult<()> {
self.get_main_widget_mut().unwrap().on_key(key); self.get_main_widget_mut().unwrap().on_key(key);
Ok(())
} }
} }

View File

@ -145,7 +145,7 @@ impl Widget for MiniBuffer {
self.input) self.input)
} }
fn on_key(&mut self, key: Key) { fn on_key(&mut self, key: Key) -> HResult<()> {
match key { match key {
Key::Esc | Key::Ctrl('c') => { self.input.clear(); self.done = true; }, Key::Esc | Key::Ctrl('c') => { self.input.clear(); self.done = true; },
Key::Char('\n') => { Key::Char('\n') => {
@ -205,7 +205,8 @@ impl Widget for MiniBuffer {
self.input.insert(self.position, key); self.input.insert(self.position, key);
self.position += 1; self.position += 1;
} }
_ => {} _ => { }
} }
Ok(())
} }
} }

View File

@ -185,12 +185,12 @@ impl<T: Widget + Send + 'static> Widget for WillBeWidget<T> {
let widget = widget.as_ref().unwrap(); let widget = widget.as_ref().unwrap();
widget.get_drawlist() widget.get_drawlist()
} }
fn on_key(&mut self, key: termion::event::Key) { fn on_key(&mut self, key: termion::event::Key) -> HResult<()> {
if self.willbe.check().is_err() { return } if self.willbe.check().is_err() { return Ok(()) }
let widget = self.widget().unwrap(); let widget = self.widget().unwrap();
let mut widget = widget.try_lock().unwrap(); let mut widget = widget.try_lock().unwrap();
let widget = widget.as_mut().unwrap(); let widget = widget.as_mut().unwrap();
widget.on_key(key); widget.on_key(key)
} }
} }

250
src/proclist.rs Normal file
View File

@ -0,0 +1,250 @@
use std::sync::{Arc, Mutex};
use std::process::Child;
use std::process::Stdio;
use std::os::unix::io::FromRawFd;
use std::io::{BufRead, BufReader};
use termion::event::Key;
use unicode_width::UnicodeWidthStr;
use crate::coordinates::{Coordinates, Size, Position};
use crate::listview::{Listable, ListView};
use crate::textview::TextView;
use crate::widget::Widget;
use crate::window::{send_event, Events};
use crate::fail::{HResult, HError};
use crate::term;
#[derive(Debug)]
struct Process {
cmd: String,
handle: Arc<Mutex<Child>>,
output: Arc<Mutex<String>>,
status: Arc<Mutex<Option<i32>>>,
success: Arc<Mutex<Option<bool>>>
}
impl Process {
fn read_proc(&mut self) -> HResult<()> {
let handle = self.handle.clone();
let output = self.output.clone();
let status = self.status.clone();
let success = self.success.clone();
std::thread::spawn(move || {
let stdout = handle.lock().unwrap().stdout.take().unwrap();
let mut stdout = BufReader::new(stdout);
loop {
let mut line = String::new();
match stdout.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
output.lock().unwrap().push_str(&line);
send_event(Events::WidgetReady).unwrap();
}
Err(err) => {
dbg!(err);
break;
}
}
}
if let Ok(proc_status) = handle.lock().unwrap().wait() {
*success.lock().unwrap() = Some(proc_status.success());
*status.lock().unwrap() = proc_status.code();
}
});
Ok(())
}
}
impl Listable for ListView<Vec<Process>> {
fn len(&self) -> usize { self.content.len() }
fn render(&self) -> Vec<String> {
self.content.iter().map(|proc| {
self.render_proc(proc)
}).collect()
}
}
impl ListView<Vec<Process>> {
fn run_proc(&mut self, cmd: &str) -> HResult<()> {
let handle = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(unsafe { Stdio::from_raw_fd(2) })
.spawn()?;
let mut proc = Process {
cmd: cmd.to_string(),
handle: Arc::new(Mutex::new(handle)),
output: Arc::new(Mutex::new(String::new())),
status: Arc::new(Mutex::new(None)),
success: Arc::new(Mutex::new(None))
};
proc.read_proc()?;
self.content.push(proc);
Ok(())
}
fn kill_proc(&mut self) -> HResult<()> {
let proc = self.selected_proc()?;
proc.handle.lock()?.kill()?;
Ok(())
}
fn remove_proc(&mut self) -> HResult<()> {
self.kill_proc().ok();
let selection = self.get_selection();
self.content.remove(selection);
Ok(())
}
fn selected_proc(&mut self) -> Option<&mut Process> {
let selection = self.get_selection();
self.content.get_mut(selection)
}
pub fn render_proc(&self, proc: &Process) -> String {
let status = match *proc.status.lock().unwrap() {
Some(status) => format!("{}", status),
None => "<R>".to_string()
};
let xsize = self.get_coordinates().xsize();
let sized_string = term::sized_string(&proc.cmd, xsize);
let status_pos = xsize - status.len() as u16;
let padding = sized_string.len() - sized_string.width_cjk();
let padding = xsize - padding as u16;
let color_status = match *proc.success.lock().unwrap() {
Some(false) => { format!("{}{}", term::color_red(), status) }
_ => { status }
};
format!(
"{}{}{}{}{}{}",
termion::cursor::Save,
format!("{}{}{:padding$}{}",
term::normal_color(),
&sized_string,
" ",
term::normal_color(),
padding = padding as usize),
termion::cursor::Restore,
termion::cursor::Right(status_pos),
term::highlight_color(),
color_status
)
}
}
pub struct ProcView {
coordinates: Coordinates,
proc_list: ListView<Vec<Process>>,
textview: TextView,
}
impl ProcView {
pub fn new() -> ProcView {
ProcView {
coordinates: Coordinates::new(),
proc_list: ListView::new(vec![]),
textview: TextView::new_blank(),
}
}
pub fn run_proc(&mut self, cmd: &str) -> HResult<()> {
self.proc_list.run_proc(cmd)?;
Ok(())
}
pub fn remove_proc(&mut self) -> HResult<()> {
self.proc_list.remove_proc()?;
self.textview.set_text("");
Ok(())
}
fn show_output(&mut self) -> HResult<()> {
let output = self.proc_list.selected_proc()?.output.lock()?;
self.textview.set_text(&*output);
Ok(())
}
pub fn calculate_coordinates(&self) -> (Coordinates, Coordinates) {
let xsize = self.coordinates.xsize();
let ysize = self.coordinates.ysize();
let top = self.coordinates.top().y();
let ratio = (33, 66);
let left_xsize = xsize * ratio.0 / 100;
let left_size = Size((left_xsize, ysize));
let left_pos = self.coordinates.top();
let main_xsize = xsize * ratio.1 / 100;
let main_size = Size((main_xsize, ysize));
let main_pos = Position((left_xsize + 2, top));
let left_coords = Coordinates {
size: left_size,
position: left_pos,
};
let main_coords = Coordinates {
size: main_size,
position: main_pos,
};
(left_coords, main_coords)
}
}
impl Widget for ProcView {
fn get_coordinates(&self) -> &Coordinates {
&self.coordinates
}
fn set_coordinates(&mut self, coordinates: &Coordinates) {
self.coordinates = coordinates.clone();
let (lcoord, rcoord) = self.calculate_coordinates();
self.proc_list.set_coordinates(&lcoord);
self.textview.set_coordinates(&rcoord);
self.refresh();
}
fn render_header(&self) -> String {
"".to_string()
}
fn refresh(&mut self) {
self.show_output().ok();
self.proc_list.refresh();
self.textview.refresh();
}
fn get_drawlist(&self) -> String {
self.proc_list.get_drawlist() + &self.textview.get_drawlist()
}
fn on_key(&mut self, key: Key) -> HResult<()> {
match key {
Key::Char('w') => { return Err(HError::PopupFinnished) }
Key::Char('d') => { self.remove_proc()? }
Key::Char('k') => { self.proc_list.kill_proc()? }
Key::Up | Key::Char('p') => {
self.proc_list.move_up();
self.proc_list.refresh();
}
Key::Down | Key::Char('n') => {
self.proc_list.move_down();
self.proc_list.refresh();
}
_ => {}
}
self.refresh();
self.draw()?;
Ok(())
}
}

View File

@ -2,6 +2,7 @@ use termion::event::Key;
use crate::coordinates::{Coordinates}; use crate::coordinates::{Coordinates};
use crate::widget::Widget; use crate::widget::Widget;
use crate::fail::HResult;
pub trait Tabbable { pub trait Tabbable {
fn new_tab(&mut self); fn new_tab(&mut self);
@ -129,7 +130,8 @@ impl<T> Widget for TabView<T> where T: Widget, TabView<T>: Tabbable {
self.refresh(); self.refresh();
} }
fn on_key(&mut self, key: Key) { fn on_key(&mut self, key: Key) -> HResult<()> {
Tabbable::on_key(self, key); Tabbable::on_key(self, key);
Ok(())
} }
} }

View File

@ -48,6 +48,12 @@ impl TextView {
coordinates: Coordinates::new(), coordinates: Coordinates::new(),
} }
} }
pub fn set_text(&mut self, text: &str) {
let lines = text.lines().map(|l| l.to_string()).collect();
self.lines = lines;
self.refresh();
}
} }
impl Widget for TextView { impl Widget for TextView {

View File

@ -1,9 +1,13 @@
use std::sync::mpsc::channel;
use termion::event::{Event, Key, MouseEvent}; use termion::event::{Event, Key, MouseEvent};
use termion::input::TermRead;
use crate::coordinates::{Coordinates, Position, Size}; use crate::coordinates::{Coordinates, Position, Size};
use crate::fail::HResult; use crate::fail::{HResult, HError};
use crate::window::{send_event, Events};
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write, stdin};
pub trait Widget { pub trait Widget {
@ -18,7 +22,7 @@ pub trait Widget {
fn get_drawlist(&self) -> String; fn get_drawlist(&self) -> String;
fn on_event(&mut self, event: Event) { fn on_event(&mut self, event: Event) -> HResult<()> {
match event { match event {
Event::Key(Key::Char('q')) => panic!("It's your fault!"), Event::Key(Key::Char('q')) => panic!("It's your fault!"),
Event::Key(key) => self.on_key(key), Event::Key(key) => self.on_key(key),
@ -27,22 +31,25 @@ pub trait Widget {
} }
} }
fn on_key(&mut self, key: Key) { fn on_key(&mut self, key: Key) -> HResult<()> {
match key { match key {
_ => self.bad(Event::Key(key)), _ => self.bad(Event::Key(key)),
} }
Ok(())
} }
fn on_mouse(&mut self, event: MouseEvent) { fn on_mouse(&mut self, event: MouseEvent) -> HResult<()> {
match event { match event {
_ => self.bad(Event::Mouse(event)), _ => self.bad(Event::Mouse(event)),
} }
Ok(())
} }
fn on_wtf(&mut self, event: Vec<u8>) { fn on_wtf(&mut self, event: Vec<u8>) -> HResult<()> {
match event { match event {
_ => self.bad(Event::Unsupported(event)), _ => self.bad(Event::Unsupported(event)),
} }
Ok(())
} }
fn show_status(&self, status: &str) { fn show_status(&self, status: &str) {
@ -125,6 +132,49 @@ pub trait Widget {
Ok(()) Ok(())
} }
fn popup(&mut self) -> HResult<()> {
self.run_widget();
send_event(Events::ExclusiveEvent(None));
Ok(())
}
fn run_widget(&mut self) -> HResult<()> {
let (tx_event, rx_event) = channel();
send_event(Events::ExclusiveEvent(Some(tx_event)))?;
dbg!("sent exclusive request");
self.clear()?;
self.refresh();
self.draw()?;
dbg!("entering loop");
for event in rx_event.iter() {
dbg!(&event);
match event {
Events::InputEvent(input) => {
if let Err(HError::PopupFinnished) = self.on_event(input) {
return Err(HError::PopupFinnished)
}
}
Events::WidgetReady => {
self.refresh();
}
_ => {}
}
self.draw()?;
}
Ok(())
}
fn clear(&self) -> HResult<()> {
let clearlist = self.get_clearlist();
write!(std::io::stdout(), "{}", clearlist)?;
std::io::stdout().flush()?;
Ok(())
}
fn animate_slide_up(&mut self) { fn animate_slide_up(&mut self) {
let coords = self.get_coordinates().clone(); let coords = self.get_coordinates().clone();
let xpos = coords.position().x(); let xpos = coords.position().x();

View File

@ -20,11 +20,12 @@ lazy_static! {
= Arc::new(Mutex::new(MiniBuffer::new())); = Arc::new(Mutex::new(MiniBuffer::new()));
} }
#[derive(Debug)]
pub enum Events { pub enum Events {
InputEvent(Event), InputEvent(Event),
WidgetReady, WidgetReady,
ExclusiveInput(bool), ExclusiveInput(bool),
ExclusiveEvent(Option<Sender<Events>>),
} }
pub struct Window<T> pub struct Window<T>
@ -87,32 +88,22 @@ where
pub fn handle_input(&mut self) { pub fn handle_input(&mut self) {
let (tx_event_internal, rx_event_internal) = channel();
let (tx_event, rx_event) = channel(); let (tx_event, rx_event) = channel();
*TX_EVENT.try_lock().unwrap() = Some(tx_event); let (tx_global_event, rx_global_event) = channel();
let (tx_request_input, rx_request_input) = channel(); *TX_EVENT.try_lock().unwrap() = Some(tx_global_event);
let (tx_internal_event, rx_internal_event) = channel();
let mut exclusive_mode = false; input_thread(tx_event.clone());
global_event_thread(rx_global_event, tx_event.clone());
dispatch_events(rx_event, tx_internal_event);
event_thread(rx_event, tx_event_internal.clone()); for event in rx_internal_event.iter() {
input_thread(tx_event_internal.clone(), rx_request_input);
tx_request_input.send(()).unwrap();
for event in rx_event_internal.iter() {
//Self::clear_status();
//let event = event.unwrap();
match event { match event {
Events::InputEvent(event) => { Events::InputEvent(event) => {
self.widget.on_event(event); self.widget.on_event(event);
self.screen.cursor_hide(); self.screen.cursor_hide();
self.draw(); self.draw();
if !exclusive_mode {
tx_request_input.send(()).unwrap();
}
}, },
Events::ExclusiveInput(setting) => {
exclusive_mode = setting
}
_ => { _ => {
self.widget.refresh(); self.widget.refresh();
self.draw(); self.draw();
@ -122,23 +113,39 @@ where
} }
} }
fn event_thread(rx: Receiver<Events>, fn dispatch_events(rx: Receiver<Events>, tx: Sender<Events>) {
tx: Sender<Events>) {
std::thread::spawn(move || { std::thread::spawn(move || {
let mut tx_exclusive_event: Option<Sender<Events>> = None;
for event in rx.iter() { for event in rx.iter() {
match &event {
Events::ExclusiveEvent(tx_event) => {
tx_exclusive_event = tx_event.clone();
}
_ => {}
}
if let Some(tx_event) = &tx_exclusive_event {
tx_event.send(event).unwrap();
} else {
tx.send(event).unwrap();
}
}
});
}
fn global_event_thread(rx_global: Receiver<Events>,
tx: Sender<Events>) {
std::thread::spawn(move || {
for event in rx_global.iter() {
tx.send(event).unwrap(); tx.send(event).unwrap();
} }
}); });
} }
fn input_thread(tx: Sender<Events>, request_input: Receiver<()>) { fn input_thread(tx: Sender<Events>) {
std::thread::spawn(move || { std::thread::spawn(move || {
for _ in request_input.iter() { for input in stdin().events() {
for input in stdin().events() { let input = input.unwrap();
let input = input.unwrap(); tx.send(Events::InputEvent(input)).unwrap();
tx.send(Events::InputEvent(input)).unwrap();
break;
}
} }
}); });
} }