diff --git a/src/ui/cli_hooker.rs b/src/ui/cli_hooker.rs index e69de29..2a3da1e 100644 --- a/src/ui/cli_hooker.rs +++ b/src/ui/cli_hooker.rs @@ -0,0 +1,203 @@ +use std::{ + error::Error, + sync::{ + mpsc::{Receiver, Sender}, + Arc, Mutex, + }, + thread::{sleep, spawn, JoinHandle}, + time::Duration, +}; + +use pyo3::exceptions::PyRuntimeError; + +use crate::{ + cli::tty::Tty, + consts::DURATION, + err, impl_any, + pythonapi::shell_like::{PyTtyWrapper, TtyType}, +}; + +use super::terminal::TerminalMessage; + +pub struct UiCliWrapper { + inner: Arc>>>, + buf: Arc>>, + sender: Arc>>>, + receiver: Arc>>>, + stop: Arc>, + handle: Option>, +} + +impl UiCliWrapper { + pub fn build(inner: *mut TtyType) -> Self { + let buf = Arc::new(Mutex::new(Vec::new())); + let sender = Arc::new(Mutex::new(None)); + let receiver = Arc::new(Mutex::new(None)); + let inner = Some(unsafe { Box::from_raw(inner) }); + let mut res = Self { + inner: Arc::new(Mutex::new(inner)), + buf, + sender, + receiver, + stop: Arc::new(Mutex::new(false)), + handle: None, + }; + + let inner = res.inner.clone(); + let buf = res.buf.clone(); + let sender = res.sender.clone(); + let receiver = res.receiver.clone(); + let stop = res.stop.clone(); + + let handler = spawn(move || loop { + sleep(Duration::from_millis(DURATION)); + { + let stop = stop.lock().unwrap(); + if *stop { + break; + } + } + // read into buffer + { + let mut inner = inner.lock().unwrap(); + if inner.is_none() { + continue; + } + let inner = inner.as_mut().unwrap(); + let data = inner.read(); + if let Err(e) = data { + err!("read error: {}", e); + break; + } + let data = data.unwrap(); + let mut buf = buf.lock().unwrap(); + buf.extend(data); + } + // recv stop signal from terminal + { + let mut recv_o = receiver.lock().unwrap(); + if recv_o.is_none() { + continue; + } + let recv = recv_o.as_mut().unwrap(); + match recv.try_recv() { + Ok(TerminalMessage::Data(_)) => { + unreachable!() + } + Ok(TerminalMessage::Close) => { + let mut send_o = sender.lock().unwrap(); + let send = send_o.as_ref().unwrap(); + send.send(TerminalMessage::Close).unwrap(); + send_o.take(); + recv_o.take(); + } + Err(_) => {} + } + } + }); + + res.handle = Some(handler); + + res + } +} + +impl Drop for UiCliWrapper { + fn drop(&mut self) { + let mut stop = self.stop.lock().unwrap(); + *stop = true; + } +} +impl_any!(UiCliWrapper); +impl Tty for UiCliWrapper { + fn read(&mut self) -> Result, Box> { + let mut buf = self.buf.lock().unwrap(); + let res = buf.clone(); + buf.clear(); + Ok(res) + } + fn read_line(&mut self) -> Result, Box> { + let mut buf = self.buf.lock().unwrap(); + let mut res = Vec::new(); + for &c in buf.iter() { + if c == b'\n' { + break; + } + res.push(c); + } + buf.drain(..res.len()); + Ok(res) + } + fn write(&mut self, data: &[u8]) -> Result<(), Box> { + let mut inner = self.inner.lock().unwrap(); + if inner.is_none() { + return Err("tty is closed".into()); + } + let inner = inner.as_mut().unwrap(); + inner.write(data) + } +} + +pub struct PyUiCliWrapper { + inner: TtyType, +} + +impl PyUiCliWrapper { + fn inner_spec(&self) -> &Box { + let inner = &self.inner; + inner.as_any().downcast_ref::>().unwrap() + } + fn inner_spec_mut(&mut self) -> &mut Box { + let inner = &mut self.inner; + inner + .as_any_mut() + .downcast_mut::>() + .unwrap() + } +} + +impl PyTtyWrapper for PyUiCliWrapper { + fn take(&mut self) -> pyo3::PyResult<*mut TtyType> { + let inner = self.inner_spec_mut(); + let mut inner = inner.inner.lock().unwrap(); + let inner = inner.take(); + if inner.is_none() { + return Err(PyRuntimeError::new_err("tty is closed")); + } + let inner = inner.unwrap(); + let inner = Box::into_raw(inner); + Ok(inner) + } + fn safe_take(&mut self) -> pyo3::PyResult> { + let res = self.take()?; + Ok(unsafe { Box::from_raw(res) }) + } + fn get(&self) -> pyo3::PyResult<&TtyType> { + let inner = self.inner_spec(); + let inner = inner.inner.lock().unwrap(); + if inner.is_none() { + return Err(PyRuntimeError::new_err("tty is closed")); + } + Ok(&self.inner) + } + fn get_mut(&mut self) -> pyo3::PyResult<&mut TtyType> { + { + let inner = self.inner_spec(); + let inner = inner.inner.lock().unwrap(); + if inner.is_none() { + return Err(PyRuntimeError::new_err("tty is closed")); + } + } + Ok(&mut self.inner) + } + fn put(&mut self, tty: *mut TtyType) -> pyo3::PyResult<()> { + let inner = self.inner_spec_mut(); + let mut inner = inner.inner.lock().unwrap(); + if inner.is_some() { + return Err(PyRuntimeError::new_err("tty is already opened")); + } + let tty = unsafe { Box::from_raw(tty) }; + *inner = Some(tty); + Ok(()) + } +} diff --git a/src/ui/main.rs b/src/ui/main.rs index 76e8f01..d07e1a6 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -7,7 +7,7 @@ use eframe::{ run_native, App, Frame, NativeOptions, }; -use crate::info; +use crate::{info, util::anybase::AnyBase}; /// Main UI struct /// @@ -107,12 +107,13 @@ impl App for MyApp { /// This trait is used to hold the sub window. /// This is because the egui is a immediate mode UI. when you display a new window, it will be shown THAT particular frame. if you need that window to stay, you need to create a new window AGAIN next frame too and egui using the window's name (or other id source), egui will internally keep track of its position / focus status etc.. /// So, we need some way to keep track of the sub window. -pub trait SubWindow { +pub trait SubWindow: AnyBase { /// Show the window, this will be called every frame. Your window is identified by the `id` parameter. /// However, that doesn't mean you should change the title, as this contains the window number, useful for the user. fn show(&mut self, ctx: &Context, title: &str, id: &Id, open: &mut bool); } +#[doc(hidden)] pub trait SubWindowCreator { fn name(&self) -> &str; fn open(&self) -> Box; @@ -155,6 +156,8 @@ macro_rules! impl_sub_window { create: ${concat(create_, $name)}, } } + + $crate::impl_any!($name); }; } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 50fbd90..56bf727 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,4 +8,5 @@ pub mod main; pub mod code_editor; pub mod pyenv; -pub mod terminal; \ No newline at end of file +pub mod terminal; +pub mod cli_hooker; \ No newline at end of file diff --git a/src/ui/terminal.rs b/src/ui/terminal.rs index 19b9e75..ac9ff03 100644 --- a/src/ui/terminal.rs +++ b/src/ui/terminal.rs @@ -1,22 +1,193 @@ -use eframe::egui::{Context, Id, Ui, Window}; +use std::{ + cmp::{max, min}, + error::Error, + ops::Range, + sync::{ + mpsc::{self, Receiver, Sender}, + Arc, Mutex, + }, + thread::{sleep, spawn, JoinHandle}, + time::Duration, +}; -use crate::impl_sub_window; +use eframe::egui::{ + scroll_area::ScrollBarVisibility, Context, FontId, Id, RichText, ScrollArea, Ui, Window, +}; + +use crate::{consts::DURATION, impl_sub_window}; use super::main::SubWindow; +pub enum TerminalMessage { + Data(Vec), + Close, +} + pub struct Terminal { size: (u32, u32), + buf: Arc>>, + send: Arc>>>, // Only Close will be sent, indicating that the terminal is closed + recv: Arc>>>, + handle: Option>, + stop: Arc>, } impl Default for Terminal { fn default() -> Self { - Terminal { size: (24, 80) } + let test_data = b"Hello, World!\n".to_vec(); + + let buf = Arc::new(Mutex::new(test_data)); + let mut res = Terminal { + size: (24, 80), + buf, + send: Arc::new(Mutex::new(None)), + recv: Arc::new(Mutex::new(None)), + handle: None, + stop: Arc::new(Mutex::new(false)), + }; + + let buf = res.buf.clone(); + let send = res.send.clone(); + let recv = res.recv.clone(); + let stop = res.stop.clone(); + + let handler = spawn(move || loop { + sleep(Duration::from_millis(DURATION)); + { + let stop = stop.lock().unwrap(); + if *stop { + break; + } + } + let mut recv_o = recv.lock().unwrap(); + if recv_o.is_none() { + continue; + } + let recv = recv_o.as_mut().unwrap(); + match recv.try_recv() { + Ok(TerminalMessage::Data(data)) => { + let mut buf = buf.lock().unwrap(); + buf.extend(data); + } + Ok(TerminalMessage::Close) => { + let mut send_o = send.lock().unwrap(); + let send = send_o.as_ref().unwrap(); + send.send(TerminalMessage::Close).unwrap(); + recv_o.take(); + send_o.take(); + let mut buf = buf.lock().unwrap(); + buf.clear(); + buf.extend(b"Hello, world!\n"); + } + Err(_) => { + continue; + } + } + }); + + res.handle = Some(handler); + + res + } +} + +impl Drop for Terminal { + fn drop(&mut self) { + let mut stop = self.stop.lock().unwrap(); + *stop = true; + let send = self.send.lock().unwrap(); + if let Some(send) = send.as_ref() { + send.send(TerminalMessage::Close).unwrap(); + } } } impl Terminal { + pub fn try_hook( + &mut self, + recv: Receiver, + ) -> Result, Box> { + let mut recv_o = self.recv.lock().unwrap(); + if recv_o.is_some() { + return Err("Terminal already hooked".into()); + } + *recv_o = Some(recv); + let (rsend, rrecv) = mpsc::channel(); + let mut send = self.send.lock().unwrap(); + *send = Some(rsend); + Ok(rrecv) + } +} + +impl Terminal { + fn gc_bufs(&mut self) { + // Remove some old data + let buf = self.buf.clone(); + let mut buf = buf.lock().unwrap(); + let lines = buf.iter().filter(|&&c| c == b'\n').count(); + if lines < 100 { + return; + } + let drain_line = lines - 100; + let lst_pos = buf + .iter() + .enumerate() + .filter(|&(_, &c)| c == b'\n') + .nth(drain_line) + .unwrap() + .0; + buf.drain(0..lst_pos); + } + fn render_row(&mut self, ui: &mut Ui, rows: Range) { + self.gc_bufs(); + // ui.label("01234567890123456789012345678901234567890123456789012345678901234567890123456789"); + // Use this to make the line above the same length as the terminal + ui.label(" "); + let buf = self.buf.clone(); + let buf = buf.lock().unwrap(); + let mut data = Vec::from([0]); + data.extend( + buf.iter() + .enumerate() + .filter(|&(_, &c)| c == b'\n') + .map(|(i, _)| i), + ); + if !buf[buf.len() - 1] == b'\n' { + data.push(buf.len()); + } + let begin_line = min(data.len() - 1, rows.start); + let end_line = min(data.len() - 1, rows.end); + let begin_pos = data[begin_line]; + let end_pos = data[end_line]; + let row = &buf[begin_pos..end_pos]; + let row = String::from_utf8_lossy(row); + let text = RichText::new(row).monospace().font(FontId::monospace(14.0)); + ui.label(text); + } + + fn render_term(&mut self, ui: &mut Ui) { + // For now, the term has no style... Mabe Ansi to egui style? + + let total_rows = { + let buf = self.buf.clone(); + let buf = buf.lock().unwrap(); + buf.iter().filter(|&&c| c == b'\n').count() + }; + let total_rows = max(self.size.0 as usize, total_rows); + + ScrollArea::vertical() + .max_height(self.size.0 as f32 * 18.0) + .max_width(self.size.1 as f32 * 18.0) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) + .stick_to_bottom(true) + .show_rows(ui, 14.0, total_rows, |ui, rows| self.render_row(ui, rows)); + + ui.ctx().request_repaint(); + } fn show(&mut self, ui: &mut Ui) { ui.label("Terminal"); + + self.render_term(ui); } } @@ -25,6 +196,10 @@ impl SubWindow for Terminal { let window = Window::new(title) .id(id.to_owned()) .open(open) + .hscroll(false) + .vscroll(false) + .min_height(self.size.0 as f32 * 20.0) + .min_width(self.size.1 as f32 * 20.0) .resizable([false, false]); window.show(ctx, |ui| { self.show(ui);