use crate::terminal_emulation::TerminalParser;
use anyhow::anyhow;
use appcui::dialogs::{Location, OpenFileDialogFlags, SelectFolderDialogFlags};
use appcui::graphics::{CharAttribute, CharFlags, Character, Color, Size, Surface};
use appcui::prelude::window::Flags;
use appcui::prelude::{canvas, Alignment, Canvas, EventProcessStatus, Handle, LayoutBuilder, OnResize, TimerEvents, Window};
use async_channel::{Receiver, Sender};
use std::ffi::OsStr;
use std::path::Path;
use std::time::Duration;
use virtual_terminal::{Command, Input, Output};
use crate::shortcut::{BackgroundColor, TerminalOptions, WindowOptions, WindowSize};
#[CustomControl(overwrite = OnKeyPressed)]
pub struct CustomKeyboardControl {
pub should_exit: bool,
pub tx: Sender<Input>,
pub rx: Receiver<Output>,
}
#[Window(events = TimerEvents)]
pub struct TuiWindow {
pub canvas: Handle<Canvas>,
pub terminal_parser: TerminalParser,
pub custom_keyboard_control: Handle<CustomKeyboardControl>,
pub horizontal_adjustment: u32,
pub vertical_adjustment: u32
}
impl TuiWindow {
pub fn new<S, I>(
app_name: &str,
program: S,
args: I,
window_options: WindowOptions,
terminal_options: TerminalOptions,
) -> anyhow::Result<Self> where S: AsRef<OsStr>, I: IntoIterator<Item = S> {
let window_size = window_options.size
.unwrap_or(WindowSize {
width: 100,
height: 25,
});
let mut x = 0;
let mut y = 0;
let mut horizontal_adjustment: i32 = 2;
let mut vertical_adjustment: i32 = 2;
if let Some(padding) = terminal_options.padding {
x = padding.0;
y = padding.1;
horizontal_adjustment += padding.0;
vertical_adjustment += padding.1;
}
let inner_size = Size {
width: window_size.width.saturating_sub(horizontal_adjustment as u32),
height: window_size.height.saturating_sub(vertical_adjustment as u32),
};
let mut window_flags = Flags::None;
if window_options.resizable {
window_flags |= Flags::Sizeable;
}
if !window_options.close_button {
window_flags |= Flags::NoCloseButton;
}
if window_options.fixed_position {
window_flags |= Flags::FixedPosition;
}
let win = Window::new(
app_name,
LayoutBuilder::new()
.alignment(Alignment::Center)
.width(window_size.width)
.height(window_size.height)
.build(),
window_flags
);
let mut modified_program = replace_file_path(program.as_ref().to_str().unwrap().to_string())?;
modified_program = replace_folder_path(modified_program)?;
let mut modified_args: Vec<String> = Vec::new();
for arg in args {
let mut modified_arg = replace_file_path(arg.as_ref().to_str().unwrap().to_string())?;
modified_arg = replace_folder_path(modified_arg)?;
modified_args.push(modified_arg);
}
let cmd = Command::new(modified_program)
.args(modified_args)
.terminal_size((
inner_size.width as usize,
inner_size.height as usize
));
let rx = cmd.out_rx();
let tx = cmd.in_tx();
tx.send_blocking(Input::Resize((
inner_size.width as usize,
inner_size.height as usize
)))?;
let default_background_color = match terminal_options.background_color {
None => Color::RGB(0, 0, 0),
Some(BackgroundColor { r, g, b }) => Color::RGB(r, g, b),
};
let mut tui_win = Self {
base: win,
canvas: Handle::None,
custom_keyboard_control: Handle::None,
terminal_parser: TerminalParser::new(
window_size.width,
window_size.height,
default_background_color
),
horizontal_adjustment: horizontal_adjustment as u32,
vertical_adjustment: vertical_adjustment as u32,
};
tui_win.canvas = tui_win.add(Canvas::new(
Size::new(inner_size.width, inner_size.height),
LayoutBuilder::new()
.width(inner_size.width)
.height(inner_size.height)
.x(x)
.y(y)
.build(),
canvas::Flags::None
));
let c = tui_win.canvas;
if let Some(cv) = tui_win.control_mut(c) {
let surface = cv.drawing_surface_mut();
surface.fill_rect(
Rect::new(0, 0, inner_size.width as i32, inner_size.height as i32),
Character::new(' ', Color::Transparent, default_background_color, CharFlags::None)
);
surface.write_string(0, 0, "Loading...", CharAttribute::default(), false);
}
let timer = match tui_win.timer() {
Some(t) => t,
None => return Err(anyhow!("Failed to get timer"))
};
timer.start(Duration::from_millis(25));
tui_win.custom_keyboard_control = tui_win.add(CustomKeyboardControl {
should_exit: false,
base: ControlBase::new(Layout::fill(), true),
tx,
rx,
});
tokio::spawn(cmd.run());
let c = tui_win.canvas;
if let Some(cv) = tui_win.control_mut(c) {
let surface = cv.drawing_surface_mut();
surface.clear(Character::new(' ', Color::Transparent, Color::Transparent, CharFlags::None));
}
Ok(tui_win)
}
pub fn close_command(&mut self) {
let custom_keyboard_control = self.custom_keyboard_control;
let control = self.control_mut(custom_keyboard_control).unwrap();
control.tx.send_blocking(Input::Terminate).ok();
control.tx.close();
control.rx.close();
self.close();
}
}
impl TimerEvents for TuiWindow {
fn on_update(&mut self, _: u64) -> EventProcessStatus {
let (should_close, (rx_clone, tx_clone)) = {
let ckc = self.control(self.custom_keyboard_control).unwrap();
(ckc.should_exit, (ckc.rx.clone(), ckc.tx.clone()))
};
if should_close {
self.close_command();
return EventProcessStatus::Processed;
}
match rx_clone.try_recv() {
Ok(msg) => match msg {
Output::Pid(_) => EventProcessStatus::Ignored,
Output::Stdout(command_output) => {
let size = self.size();
let inner_size = Size {
width: size.width.saturating_sub(self.horizontal_adjustment),
height: size.height.saturating_sub(self.vertical_adjustment),
};
let (old_surface, should_resize) = {
let c = self.canvas;
let cv = self.control_mut(c).unwrap();
let should_resize = cv.size() != inner_size;
let surface = cv.drawing_surface_mut();
let mut buffer = Vec::new();
surface.serialize_to_buffer(&mut buffer);
(Surface::from_buffer(&buffer).unwrap(), should_resize)
};
let new_surface = self.terminal_parser.parse_to_surface(&command_output, old_surface);
let c = self.canvas;
let cv = self.control_mut(c).unwrap();
let surface = cv.drawing_surface_mut();
*surface = new_surface;
if should_resize {
tx_clone
.send_blocking(Input::Resize((
inner_size.width as usize,
inner_size.height as usize
)))
.ok();
cv.set_size(inner_size.width as u16, inner_size.height as u16);
cv.resize_surface(inner_size);
self.terminal_parser.resize(inner_size.width, inner_size.height);
}
EventProcessStatus::Processed
}
Output::Error(error) => {
dialogs::error("An error occurred", &error);
self.close();
EventProcessStatus::Processed
},
Output::Terminated(_) => {
self.close();
EventProcessStatus::Processed
}
}
Err(_) => EventProcessStatus::Ignored
}
}
}
fn replace_file_path(arg: String) -> anyhow::Result<String> {
match arg.contains("<FILE_PATH>") {
false => Ok(arg),
true => match dialogs::open(
"Select file",
"",
Location::Path(Path::new(env!("HOME"))),
None,
OpenFileDialogFlags::Icons | OpenFileDialogFlags::CheckIfFileExists
) {
None => Err(anyhow!("No file selected")),
Some(file_path) => Ok(arg.replace("<FILE_PATH>", file_path.to_str().unwrap()))
}
}
}
fn replace_folder_path(arg: String) -> anyhow::Result<String> {
match arg.contains("<FOLDER_PATH>") {
false => Ok(arg),
true => match dialogs::select_folder(
"Select folder",
Location::Path(Path::new(env!("HOME"))),
SelectFolderDialogFlags::Icons
) {
None => Err(anyhow!("No folder selected")),
Some(file_path) => Ok(arg.replace("<FOLDER_PATH>", file_path.to_str().unwrap()))
}
}
}