1796 lines
66 KiB
Rust
1796 lines
66 KiB
Rust
/*
|
|
* meli
|
|
*
|
|
* Copyright 2017-2018 Manos Pitsidianakis
|
|
*
|
|
* This file is part of meli.
|
|
*
|
|
* meli is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* meli is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*! The application's state.
|
|
|
|
The UI crate has an Box<dyn Component>-Component-System design. The System part, is also the application's state, so they're both merged in the `State` struct.
|
|
|
|
`State` owns all the Components of the UI. In the application's main event loop, input is handed to the state in the form of `UIEvent` objects which traverse the component graph. Components decide to handle each input or not.
|
|
|
|
Input is received in the main loop from threads which listen on the stdin for user input, observe folders for file changes etc. The relevant struct is `ThreadEvent`.
|
|
*/
|
|
|
|
use super::*;
|
|
//use crate::plugins::PluginManager;
|
|
use melib::backends::{AccountHash, BackendEventConsumer};
|
|
|
|
use crate::jobs::JobExecutor;
|
|
//use crossbeam::channel::{unbounded, Receiver, Sender};
|
|
use indexmap::IndexMap;
|
|
use smallvec::SmallVec;
|
|
use std::env;
|
|
use std::io::Write;
|
|
|
|
fn get_html_element_size() -> (usize, usize) {
|
|
let window = web_sys::window().expect("no global `window` exists");
|
|
let document = window.document().expect("should have a document on window");
|
|
|
|
// Manufacture the element we're gonna append
|
|
let val = if let Some(val) = document.get_element_by_id("terminal") {
|
|
val
|
|
} else {
|
|
js_console("COULD NOT GET ELEMENT BY ID #terminal !!!");
|
|
panic!("COULD NOT GET ELEMENT BY ID #terminal !!!");
|
|
};
|
|
let val = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&val)
|
|
.expect("Could not cast to HtmlElement");
|
|
(
|
|
(val.offset_width().saturating_sub(4) / 8) as usize,
|
|
(val.offset_height().saturating_sub(8) / 17) as usize,
|
|
)
|
|
}
|
|
|
|
struct InputHandler {
|
|
//pipe: (RawFd, RawFd),
|
|
//rx: Receiver<InputCommand>,
|
|
//tx: Sender<InputCommand>,
|
|
}
|
|
|
|
impl InputHandler {
|
|
fn restore(&self) { //, tx: Sender<ThreadEvent>) {
|
|
/*
|
|
/* Clear channel without blocking. switch_to_main_screen() issues a kill when
|
|
* returning from a fork and there's no input thread, so the newly created thread will
|
|
* receive it and die. */
|
|
//let _ = self.rx.try_iter().count();
|
|
let rx = self.rx.clone();
|
|
let pipe = self.pipe.0;
|
|
thread::Builder::new()
|
|
.name("input-thread".to_string())
|
|
.spawn(move || {
|
|
get_events(
|
|
|i| {
|
|
tx.send(ThreadEvent::Input(i)).unwrap();
|
|
},
|
|
&rx,
|
|
pipe,
|
|
)
|
|
})
|
|
.unwrap();
|
|
*/
|
|
}
|
|
|
|
fn kill(&self) {
|
|
//self.tx.send(InputCommand::Kill).unwrap();
|
|
}
|
|
|
|
fn check(&mut self) {
|
|
/*
|
|
match self.control.upgrade() {
|
|
Some(_) => {}
|
|
None => {
|
|
debug!("restarting input_thread");
|
|
self.restore();
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
/// A context container for loaded settings, accounts, UI changes, etc.
|
|
pub struct Context {
|
|
pub accounts: IndexMap<AccountHash, Account>,
|
|
pub settings: Settings,
|
|
|
|
pub runtime_settings: Settings,
|
|
/// Areas of the screen that must be redrawn in the next render
|
|
pub dirty_areas: VecDeque<Area>,
|
|
|
|
/// Events queue that components send back to the state
|
|
pub replies: VecDeque<UIEvent>,
|
|
pub sender: Sender,
|
|
//receiver: Receiver<ThreadEvent>,
|
|
//input_thread: InputHandler,
|
|
job_executor: Arc<JobExecutor>,
|
|
pub children: Vec<std::process::Child>,
|
|
|
|
pub temp_files: Vec<File>,
|
|
}
|
|
|
|
impl Context {
|
|
pub fn replies(&mut self) -> smallvec::SmallVec<[UIEvent; 8]> {
|
|
self.replies.drain(0..).collect()
|
|
}
|
|
|
|
pub fn input_kill(&self) {
|
|
//self.input_thread.kill();
|
|
}
|
|
|
|
pub fn restore_input(&self) {
|
|
//self.input.restore(self.sender.clone());
|
|
}
|
|
|
|
pub fn is_online_idx(&mut self, account_pos: usize) -> Result<()> {
|
|
let Context {
|
|
ref mut accounts,
|
|
ref mut replies,
|
|
..
|
|
} = self;
|
|
let was_online = accounts[account_pos].is_online.is_ok();
|
|
let ret = accounts[account_pos].is_online();
|
|
if ret.is_ok() {
|
|
if !was_online {
|
|
debug!("inserting mailbox hashes:");
|
|
for mailbox_node in accounts[account_pos].list_mailboxes() {
|
|
debug!(
|
|
"hash & mailbox: {:?} {}",
|
|
mailbox_node.hash,
|
|
accounts[account_pos][&mailbox_node.hash].name()
|
|
);
|
|
}
|
|
accounts[account_pos].watch();
|
|
|
|
replies.push_back(UIEvent::AccountStatusChange(accounts[account_pos].hash()));
|
|
}
|
|
}
|
|
if ret.is_ok() != was_online {
|
|
replies.push_back(UIEvent::AccountStatusChange(accounts[account_pos].hash()));
|
|
}
|
|
ret
|
|
}
|
|
|
|
pub fn is_online(&mut self, account_hash: AccountHash) -> Result<()> {
|
|
let idx = self.accounts.get_index_of(&account_hash).unwrap();
|
|
self.is_online_idx(idx)
|
|
}
|
|
}
|
|
|
|
/// A State object to manage and own components and components of the UI. `State` is responsible for
|
|
/// managing the terminal and interfacing with `melib`
|
|
pub struct State {
|
|
cols: usize,
|
|
rows: usize,
|
|
|
|
grid: CellBuffer,
|
|
overlay_grid: CellBuffer,
|
|
draw_rate_limit: RateLimit,
|
|
child: Option<ForkType>,
|
|
draw_horizontal_segment_fn: fn(&mut CellBuffer, usize, usize, usize) -> (),
|
|
pub mode: UIMode,
|
|
overlay: Vec<Box<dyn Component>>,
|
|
components: Vec<Box<dyn Component>>,
|
|
pub context: Context,
|
|
//timer: thread::JoinHandle<()>,
|
|
display_messages: SmallVec<[DisplayMessage; 8]>,
|
|
display_messages_expiration_start: Option<UnixTimestamp>,
|
|
display_messages_active: bool,
|
|
display_messages_pos: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DisplayMessage {
|
|
timestamp: UnixTimestamp,
|
|
msg: String,
|
|
}
|
|
|
|
impl Drop for State {
|
|
fn drop(&mut self) {
|
|
// When done, restore the defaults to avoid messing with the terminal.
|
|
self.switch_to_main_screen();
|
|
}
|
|
}
|
|
|
|
impl State {
|
|
pub fn new(
|
|
settings: Option<Settings>,
|
|
//sender: Sender<ThreadEvent>,
|
|
//receiver: Receiver<ThreadEvent>,
|
|
) -> Result<Self> {
|
|
/*
|
|
* Create async channel to block the input-thread if we need to fork and stop it from reading
|
|
* stdin, see get_events() for details
|
|
* */
|
|
//let input_thread = unbounded();
|
|
//let input_thread_pipe = nix::unistd::pipe()
|
|
// .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)?;
|
|
let backends = Backends::new();
|
|
let s: FileSettings = toml::from_str(&crate::ENRON_CONFIG).map_err(|e| {
|
|
MeliError::new(format!(
|
|
"{}:\nConfig file contains errors: {}",
|
|
ENRON_CONFIG,
|
|
e.to_string()
|
|
))
|
|
})?;
|
|
let settings = if let Some(settings) = settings {
|
|
settings
|
|
} else {
|
|
Settings::new(Some(s))?
|
|
};
|
|
|
|
let termsize = (175, 45);
|
|
let (cols, rows) = get_html_element_size();
|
|
|
|
let sender = crate::pool::Sender::new();
|
|
let job_executor = Arc::new(JobExecutor::new(sender.clone()));
|
|
let accounts: Vec<Account> = {
|
|
let mut file_accs = settings
|
|
.accounts
|
|
.iter()
|
|
.collect::<Vec<(&String, &AccountConf)>>();
|
|
file_accs.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
file_accs
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(index, (n, a_s))| {
|
|
let sender = sender.clone();
|
|
let account_hash = {
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::Hasher;
|
|
let mut hasher = DefaultHasher::new();
|
|
hasher.write(n.as_bytes());
|
|
hasher.finish()
|
|
};
|
|
Account::new(
|
|
account_hash,
|
|
n.to_string(),
|
|
a_s.clone(),
|
|
&backends,
|
|
job_executor.clone(),
|
|
sender.clone(),
|
|
BackendEventConsumer::new(Arc::new(
|
|
move |account_hash: AccountHash, ev: BackendEvent| {
|
|
sender
|
|
.send(ThreadEvent::UIEvent(UIEvent::BackendEvent(
|
|
account_hash,
|
|
ev,
|
|
)))
|
|
.unwrap();
|
|
},
|
|
)),
|
|
)
|
|
})
|
|
.collect::<Result<Vec<Account>>>()?
|
|
};
|
|
let accounts = accounts.into_iter().map(|acc| (acc.hash(), acc)).collect();
|
|
|
|
/*
|
|
let timer = {
|
|
let sender = sender.clone();
|
|
thread::Builder::new().spawn(move || {
|
|
let sender = sender;
|
|
loop {
|
|
thread::park();
|
|
|
|
sender.send(ThreadEvent::Pulse).unwrap();
|
|
thread::sleep(std::time::Duration::from_millis(100));
|
|
}
|
|
})
|
|
}?;
|
|
|
|
timer.thread().unpark();
|
|
*/
|
|
|
|
let working = Arc::new(());
|
|
let control = Arc::downgrade(&working);
|
|
let mut s = State {
|
|
cols,
|
|
rows,
|
|
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
|
overlay_grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
|
child: None,
|
|
mode: UIMode::Normal,
|
|
components: Vec::with_capacity(8),
|
|
overlay: Vec::new(),
|
|
draw_rate_limit: RateLimit::new(1, 3),
|
|
draw_horizontal_segment_fn: State::draw_terminal,
|
|
display_messages: SmallVec::new(),
|
|
display_messages_expiration_start: None,
|
|
display_messages_pos: 0,
|
|
display_messages_active: false,
|
|
|
|
context: Context {
|
|
accounts,
|
|
settings: settings.clone(),
|
|
runtime_settings: settings,
|
|
dirty_areas: VecDeque::with_capacity(5),
|
|
replies: VecDeque::with_capacity(5),
|
|
temp_files: Vec::new(),
|
|
job_executor,
|
|
children: vec![],
|
|
sender,
|
|
//receiver,
|
|
//input: InputHandler {
|
|
//pipe: input_thread_pipe,
|
|
//rx: input_thread.1,
|
|
//tx: input_thread.0,
|
|
//},
|
|
},
|
|
};
|
|
//s.draw_rate_limit .timer .set_value(std::time::Duration::from_millis(3));
|
|
if s.context.settings.terminal.ascii_drawing {
|
|
s.grid.set_ascii_drawing(true);
|
|
s.overlay_grid.set_ascii_drawing(true);
|
|
}
|
|
|
|
s.switch_to_alternate_screen();
|
|
for i in 0..s.context.accounts.len() {
|
|
if !s.context.accounts[i].backend_capabilities.is_remote {
|
|
s.context.accounts[i].watch();
|
|
}
|
|
if s.context.is_online_idx(i).is_ok() && s.context.accounts[i].is_empty() {
|
|
//return Err(MeliError::new(format!(
|
|
// "Account {} has no mailboxes configured.",
|
|
// s.context.accounts[i].name()
|
|
//)));
|
|
}
|
|
}
|
|
s.context.restore_input();
|
|
Ok(s)
|
|
}
|
|
|
|
/*
|
|
* When we receive a mailbox hash from a watcher thread,
|
|
* we match the hash to the index of the mailbox, request a reload
|
|
* and startup a thread to remind us to poll it every now and then till it's finished.
|
|
*/
|
|
pub fn refresh_event(&mut self, event: RefreshEvent) {
|
|
let account_hash = event.account_hash;
|
|
let mailbox_hash = event.mailbox_hash;
|
|
if self.context.accounts[&account_hash]
|
|
.mailbox_entries
|
|
.contains_key(&mailbox_hash)
|
|
{
|
|
if self.context.accounts[&account_hash]
|
|
.load(mailbox_hash)
|
|
.is_err()
|
|
{
|
|
self.context.replies.push_back(UIEvent::from(event));
|
|
return;
|
|
}
|
|
let Context {
|
|
ref mut accounts, ..
|
|
} = &mut self.context;
|
|
|
|
if let Some(notification) = accounts[&account_hash].reload(event, mailbox_hash) {
|
|
if let UIEvent::Notification(_, _, _) = notification {
|
|
self.rcv_event(UIEvent::MailboxUpdate((account_hash, mailbox_hash)));
|
|
}
|
|
self.rcv_event(notification);
|
|
}
|
|
} else {
|
|
if let melib::backends::RefreshEventKind::Failure(err) = event.kind {
|
|
debug!(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Switch back to the terminal's main screen (The command line the user sees before opening
|
|
/// the application)
|
|
pub fn switch_to_main_screen(&mut self) {
|
|
/*
|
|
write!(
|
|
self.stdout(),
|
|
"{}{}{}{}",
|
|
termion::screen::ToMainScreen,
|
|
cursor::Show,
|
|
RestoreWindowTitleIconFromStack,
|
|
BracketModeEnd,
|
|
)
|
|
.unwrap();
|
|
self.flush();
|
|
self.stdout = None;
|
|
*/
|
|
}
|
|
|
|
pub fn switch_to_alternate_screen(&mut self) {
|
|
/*
|
|
let s = std::io::stdout();
|
|
|
|
let mut stdout = AlternateScreen::from(s.into_raw_mode().unwrap());
|
|
|
|
write!(
|
|
&mut stdout,
|
|
"{save_title_to_stack}{}{}{}{window_title}{}{}",
|
|
termion::screen::ToAlternateScreen,
|
|
cursor::Hide,
|
|
clear::All,
|
|
cursor::Goto(1, 1),
|
|
BracketModeStart,
|
|
save_title_to_stack = SaveWindowTitleIconToStack,
|
|
window_title = if let Some(ref title) = self.context.settings.terminal.window_title {
|
|
format!("\x1b]2;{}\x07", title)
|
|
} else {
|
|
String::new()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
self.stdout = Some(stdout);
|
|
self.flush();
|
|
*/
|
|
}
|
|
|
|
//pub fn receiver(&self) -> Receiver<ThreadEvent> {
|
|
// self.context.receiver.clone()
|
|
//}
|
|
|
|
//pub fn sender(&self) -> Sender<ThreadEvent> {
|
|
// self.context.sender.clone()
|
|
//}
|
|
|
|
pub fn restore_input(&mut self) {
|
|
self.context.restore_input();
|
|
}
|
|
|
|
/// On `SIGWNICH` the `State` redraws itself according to the new terminal size.
|
|
pub fn update_size(&mut self) {
|
|
let term_size = get_html_element_size();
|
|
if term_size.0 == 0 || term_size.1 == 0 {
|
|
return;
|
|
}
|
|
self.cols = term_size.0;
|
|
self.rows = term_size.1;
|
|
/*
|
|
let termsize = termion::terminal_size().ok();
|
|
let termcols = termsize.map(|(w, _)| w);
|
|
let termrows = termsize.map(|(_, h)| h);
|
|
if termcols.unwrap_or(72) as usize != self.cols
|
|
|| termrows.unwrap_or(120) as usize != self.rows
|
|
{
|
|
debug!(
|
|
"Size updated, from ({}, {}) -> ({:?}, {:?})",
|
|
self.cols, self.rows, termcols, termrows
|
|
);
|
|
}
|
|
self.cols = termcols.unwrap_or(72) as usize;
|
|
self.rows = termrows.unwrap_or(120) as usize;
|
|
*/
|
|
self.grid.resize(self.cols, self.rows, Cell::with_char(' '));
|
|
self.overlay_grid
|
|
.resize(self.cols, self.rows, Cell::with_char(' '));
|
|
|
|
self.rcv_event(UIEvent::Resize);
|
|
|
|
// Invalidate dirty areas.
|
|
self.context.dirty_areas.clear();
|
|
}
|
|
|
|
/// Force a redraw for all dirty components.
|
|
pub fn redraw(&mut self) {
|
|
/*
|
|
if !self.draw_rate_limit.tick() {
|
|
return;
|
|
}
|
|
*/
|
|
|
|
for i in 0..self.components.len() {
|
|
self.draw_component(i);
|
|
}
|
|
let mut areas: smallvec::SmallVec<[Area; 8]> =
|
|
self.context.dirty_areas.drain(0..).collect();
|
|
if self.display_messages_active {
|
|
let now = melib::datetime::now();
|
|
if self
|
|
.display_messages_expiration_start
|
|
.map(|t| t + 5 < now)
|
|
.unwrap_or(false)
|
|
{
|
|
self.display_messages_active = false;
|
|
self.display_messages_expiration_start = None;
|
|
areas.push((
|
|
(0, 0),
|
|
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
|
|
));
|
|
}
|
|
}
|
|
if !areas.is_empty() {
|
|
(self.draw_horizontal_segment_fn)(&mut self.grid, 0, 0, 0);
|
|
}
|
|
|
|
/*
|
|
/* Sort by x_start, ie upper_left corner's x coordinate */
|
|
areas.sort_by(|a, b| (a.0).0.partial_cmp(&(b.0).0).unwrap());
|
|
/* draw each dirty area */
|
|
let rows = self.rows;
|
|
for y in 0..rows {
|
|
let mut segment = None;
|
|
for ((x_start, y_start), (x_end, y_end)) in &areas {
|
|
if y < *y_start || y > *y_end {
|
|
continue;
|
|
}
|
|
if let Some((x_start, x_end)) = segment.take() {
|
|
(self.draw_horizontal_segment_fn)(&mut self.grid, x_start, x_end, y);
|
|
}
|
|
match segment {
|
|
ref mut s @ None => {
|
|
*s = Some((*x_start, *x_end));
|
|
}
|
|
ref mut s @ Some(_) if s.unwrap().1 < *x_start => {
|
|
(self.draw_horizontal_segment_fn)(
|
|
&mut self.grid,
|
|
s.unwrap().0,
|
|
s.unwrap().1,
|
|
y,
|
|
);
|
|
*s = Some((*x_start, *x_end));
|
|
}
|
|
ref mut s @ Some(_) if s.unwrap().1 < *x_end => {
|
|
(self.draw_horizontal_segment_fn)(
|
|
&mut self.grid,
|
|
s.unwrap().0,
|
|
s.unwrap().1,
|
|
y,
|
|
);
|
|
*s = Some((s.unwrap().1, *x_end));
|
|
}
|
|
Some((_, ref mut x)) => {
|
|
*x = *x_end;
|
|
}
|
|
}
|
|
}
|
|
if let Some((x_start, x_end)) = segment {
|
|
(self.draw_horizontal_segment_fn)(&mut self.grid, x_start, x_end, y);
|
|
}
|
|
}
|
|
*/
|
|
|
|
if self.display_messages_active {
|
|
if let Some(DisplayMessage {
|
|
ref timestamp,
|
|
ref msg,
|
|
..
|
|
}) = self.display_messages.get(self.display_messages_pos)
|
|
{
|
|
let noto_colors = crate::conf::value(&self.context, "status.notification");
|
|
use crate::melib::text_processing::{Reflow, TextProcessing};
|
|
|
|
let msg_lines = msg.split_lines_reflow(Reflow::All, Some(self.cols / 3));
|
|
let width = msg_lines
|
|
.iter()
|
|
.map(|line| line.grapheme_len() + 4)
|
|
.max()
|
|
.unwrap_or(0);
|
|
|
|
let displ_area = place_in_area(
|
|
(
|
|
(0, 0),
|
|
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
|
|
),
|
|
(width, std::cmp::min(self.rows, msg_lines.len() + 4)),
|
|
false,
|
|
false,
|
|
);
|
|
for row in self.overlay_grid.bounds_iter(displ_area) {
|
|
for c in row {
|
|
self.overlay_grid[c]
|
|
.set_ch(' ')
|
|
.set_fg(noto_colors.fg)
|
|
.set_bg(noto_colors.bg)
|
|
.set_attrs(noto_colors.attrs);
|
|
}
|
|
}
|
|
let ((x, mut y), box_displ_area_bottom_right) =
|
|
create_box(&mut self.overlay_grid, displ_area);
|
|
for line in msg_lines
|
|
.into_iter()
|
|
.chain(Some(String::new()))
|
|
.chain(Some(melib::datetime::timestamp_to_string(*timestamp, None)))
|
|
{
|
|
write_string_to_grid(
|
|
&line,
|
|
&mut self.overlay_grid,
|
|
noto_colors.fg,
|
|
noto_colors.bg,
|
|
noto_colors.attrs,
|
|
((x, y), box_displ_area_bottom_right),
|
|
Some(x),
|
|
);
|
|
y += 1;
|
|
}
|
|
|
|
if self.display_messages.len() > 1 {
|
|
write_string_to_grid(
|
|
if self.display_messages_pos == 0 {
|
|
"Next: >"
|
|
} else if self.display_messages_pos + 1 == self.display_messages.len() {
|
|
"Prev: <"
|
|
} else {
|
|
"Prev: <, Next: >"
|
|
},
|
|
&mut self.overlay_grid,
|
|
noto_colors.fg,
|
|
noto_colors.bg,
|
|
noto_colors.attrs,
|
|
((x, y), box_displ_area_bottom_right),
|
|
Some(x),
|
|
);
|
|
}
|
|
let grid_orig = self.grid.clone();
|
|
copy_area(&mut self.grid, &self.overlay_grid, displ_area, displ_area);
|
|
//for y in get_y(upper_left!(displ_area))..=get_y(bottom_right!(displ_area)) {
|
|
(self.draw_horizontal_segment_fn)(
|
|
&mut self.grid,
|
|
get_x(upper_left!(displ_area)),
|
|
get_x(bottom_right!(displ_area)),
|
|
y,
|
|
);
|
|
//}
|
|
copy_area(&mut self.grid, &grid_orig, displ_area, displ_area);
|
|
}
|
|
}
|
|
if !self.overlay.is_empty() {
|
|
let area = center_area(
|
|
(
|
|
(0, 0),
|
|
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
|
|
),
|
|
(
|
|
if self.cols / 3 > 30 {
|
|
self.cols / 3
|
|
} else {
|
|
self.cols
|
|
},
|
|
if self.rows / 5 > 10 {
|
|
self.rows / 5
|
|
} else {
|
|
self.rows
|
|
},
|
|
),
|
|
);
|
|
copy_area(&mut self.overlay_grid, &self.grid, area, area);
|
|
self.overlay
|
|
.get_mut(0)
|
|
.unwrap()
|
|
.draw(&mut self.overlay_grid, area, &mut self.context);
|
|
let grid_orig = self.grid.clone();
|
|
copy_area(&mut self.grid, &self.overlay_grid, area, area);
|
|
//for y in get_y(upper_left!(area))..=get_y(bottom_right!(area)) {
|
|
(self.draw_horizontal_segment_fn)(
|
|
&mut self.grid,
|
|
get_x(upper_left!(area)),
|
|
get_x(bottom_right!(area)),
|
|
0,
|
|
);
|
|
//}
|
|
copy_area(&mut self.grid, &grid_orig, area, area);
|
|
}
|
|
self.flush();
|
|
}
|
|
|
|
/// Draw only a specific `area` on the screen.
|
|
fn draw_horizontal_segment(grid: &mut CellBuffer, x_start: usize, x_end: usize, y: usize) {
|
|
/*
|
|
write!(
|
|
stdout,
|
|
"{}",
|
|
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
|
|
)
|
|
.unwrap();
|
|
let mut current_fg = Color::Default;
|
|
let mut current_bg = Color::Default;
|
|
let mut current_attrs = Attr::DEFAULT;
|
|
write!(stdout, "\x1B[m").unwrap();
|
|
for x in x_start..=x_end {
|
|
let c = &grid[(x, y)];
|
|
if c.attrs() != current_attrs {
|
|
c.attrs().write(current_attrs, stdout).unwrap();
|
|
current_attrs = c.attrs();
|
|
}
|
|
if c.bg() != current_bg {
|
|
c.bg().write_bg(stdout).unwrap();
|
|
current_bg = c.bg();
|
|
}
|
|
if c.fg() != current_fg {
|
|
c.fg().write_fg(stdout).unwrap();
|
|
current_fg = c.fg();
|
|
}
|
|
if !c.empty() {
|
|
write!(stdout, "{}", c.ch()).unwrap();
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
fn draw_horizontal_segment_no_color(
|
|
grid: &mut CellBuffer,
|
|
x_start: usize,
|
|
x_end: usize,
|
|
y: usize,
|
|
) {
|
|
/*
|
|
write!(
|
|
stdout,
|
|
"{}",
|
|
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
|
|
)
|
|
.unwrap();
|
|
let mut current_attrs = Attr::DEFAULT;
|
|
write!(stdout, "\x1B[m").unwrap();
|
|
for x in x_start..=x_end {
|
|
let c = &grid[(x, y)];
|
|
if c.attrs() != current_attrs {
|
|
c.attrs().write(current_attrs, stdout).unwrap();
|
|
current_attrs = c.attrs();
|
|
}
|
|
if !c.empty() {
|
|
write!(stdout, "{}", c.ch()).unwrap();
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
fn draw_terminal(grid: &mut CellBuffer, _: usize, _: usize, _: usize) {
|
|
js_console("draw_terminal called.");
|
|
use crate::melib::text_processing::TextProcessing;
|
|
use std::collections::BTreeMap;
|
|
|
|
const LETTER_WIDTH: usize = 8;
|
|
const LETTER_HEIGHT: usize = 17;
|
|
use svg_crate::node::element::{Definitions, Group, Rectangle, Style, Text, Use};
|
|
use svg_crate::node::Text as TextNode;
|
|
use svg_crate::Document;
|
|
|
|
let (width, height) = grid.size();
|
|
/*
|
|
* Format frame as follows:
|
|
* - The entire background is a big rectangle.
|
|
* - Every text piece with unified foreground color is a text element inserted into the
|
|
* `definitions` field of the svg, and then `use`ed as a reference
|
|
* - Every background piece (a slice of unified background color) is a rectangle element
|
|
* inserted along with the `use` elements
|
|
*
|
|
* Each row is arbritarily set at 17px high, and each character cell is 8 pixels wide.
|
|
* Rectangle cells each have one extra pixel (so 18px * 9px) in their dimensions in order
|
|
* to cover the spacing between cells.
|
|
*/
|
|
let mut definitions = Definitions::new();
|
|
let mut rows_group = Group::new();
|
|
let mut text = String::with_capacity(width);
|
|
/* Before creating text node out of `text` variable, escape what's necessary */
|
|
let mut escaped_text = String::with_capacity(width);
|
|
|
|
/* keep a map with used colors and write a stylesheet when we're done */
|
|
let mut classes: BTreeMap<(u8, u8, u8), usize> = BTreeMap::new();
|
|
for (row_idx, row) in grid.bounds_iter(((0, 0), (width, height))).enumerate() {
|
|
text.clear();
|
|
escaped_text.clear();
|
|
/* Each row is a <g> group element, consisting of text elements */
|
|
let mut row_group = Group::new().set("id", format!("{:x}", row_idx + 1));
|
|
/* Keep track of colors and attributes.
|
|
* - Whenever the foreground color changes, emit a text element with the accumulated
|
|
* text in the specific foreground color.
|
|
* - Whenever the backgrund color changes, emit a rectangle element filled with the
|
|
* specific background color.
|
|
*/
|
|
let mut cur_fg = Color::Default;
|
|
let mut cur_bg = Color::Default;
|
|
let mut cur_attrs = Attr::DEFAULT;
|
|
let mut prev_x_fg = 0;
|
|
let mut is_start = true;
|
|
let mut prev_x_bg = 0;
|
|
for (x, c) in row.enumerate() {
|
|
if cur_bg != grid[c].bg() || cur_fg != grid[c].fg() || cur_attrs != grid[c].attrs()
|
|
//|| (grid[c].ch() == ' ' && !is_start)
|
|
{
|
|
if cur_bg != Color::Default {
|
|
let mut rect = Rectangle::new()
|
|
.set("x", prev_x_bg * LETTER_WIDTH)
|
|
.set("y", LETTER_HEIGHT * row_idx)
|
|
.set("width", (x - prev_x_bg) * LETTER_WIDTH + 1)
|
|
//.set("bgname", format!("{:?}", cur_bg))
|
|
.set("height", LETTER_HEIGHT);
|
|
match cur_bg {
|
|
Color::Rgb(r, g, b) => {
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
rect = rect.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
Color::Default => {
|
|
unreachable!();
|
|
}
|
|
c if c.as_byte() < 16 => {
|
|
rect = rect.set("class", format!("c{}", c.as_byte()).as_str());
|
|
}
|
|
c => {
|
|
let c = c.as_byte();
|
|
let (r, g, b) = XTERM_COLORS[c as usize];
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
rect = rect.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
}
|
|
rows_group = rows_group.add(rect);
|
|
}
|
|
prev_x_bg = x;
|
|
cur_bg = grid[c].bg();
|
|
if !text.is_empty() {
|
|
let text_length = text.grapheme_width();
|
|
for c in text.chars() {
|
|
match c {
|
|
'"' => escaped_text.push_str("""),
|
|
'&' => escaped_text.push_str("&"),
|
|
'\'' => escaped_text.push_str("'"),
|
|
'<' => escaped_text.push_str("<"),
|
|
'>' => escaped_text.push_str(">"),
|
|
c => escaped_text.push(c),
|
|
}
|
|
}
|
|
let mut text_el = Text::new()
|
|
.add(TextNode::new(&escaped_text))
|
|
.set("x", prev_x_fg * LETTER_WIDTH)
|
|
.set("textLength", text_length * LETTER_WIDTH);
|
|
/*.set("fgname", format!("{:?}", cur_fg));*/
|
|
if cur_attrs.intersects(Attr::BOLD) {
|
|
text_el = text_el.set("font-weight", "bold");
|
|
}
|
|
if cur_attrs.intersects(Attr::ITALICS) {
|
|
text_el = text_el.set("font-style", "italic");
|
|
}
|
|
if cur_attrs.intersects(Attr::UNDERLINE) {
|
|
text_el = text_el.set("text-decoration", "underline");
|
|
}
|
|
if cur_attrs.intersects(Attr::DIM) {
|
|
text_el = text_el.set("font-weight", "lighter");
|
|
}
|
|
if cur_attrs.intersects(Attr::HIDDEN) {
|
|
text_el = text_el.set("display", "none");
|
|
}
|
|
match cur_fg {
|
|
Color::Default if cur_attrs.intersects(Attr::REVERSE) => {
|
|
text_el = text_el.set("class", "b");
|
|
}
|
|
Color::Default => {
|
|
text_el = text_el.set("class", "f");
|
|
}
|
|
Color::Rgb(r, g, b) => {
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
text_el = text_el.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
c if c.as_byte() < 16 => {
|
|
text_el =
|
|
text_el.set("class", format!("c{}", c.as_byte()).as_str());
|
|
}
|
|
c => {
|
|
let c = c.as_byte();
|
|
let (r, g, b) = XTERM_COLORS[c as usize];
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
text_el = text_el.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
};
|
|
row_group = row_group.add(text_el);
|
|
text.clear();
|
|
escaped_text.clear();
|
|
}
|
|
prev_x_fg = x;
|
|
cur_fg = grid[c].fg();
|
|
cur_attrs = grid[c].attrs();
|
|
}
|
|
match grid[c].ch() {
|
|
' ' if is_start => {
|
|
prev_x_fg = x + 1;
|
|
}
|
|
c => text.push(c),
|
|
}
|
|
if grid[c].ch() != ' ' {
|
|
is_start = false;
|
|
}
|
|
}
|
|
/* Append last elements of the row if any */
|
|
if cur_bg != Color::Default {
|
|
let mut rect = Rectangle::new()
|
|
.set("x", prev_x_bg * LETTER_WIDTH)
|
|
.set("y", LETTER_HEIGHT * row_idx)
|
|
.set("width", (width - prev_x_bg) * LETTER_WIDTH + 1)
|
|
//.set("bgname", format!("{:?}", cur_bg))
|
|
.set("height", LETTER_HEIGHT);
|
|
match cur_bg {
|
|
Color::Rgb(r, g, b) => {
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
rect = rect.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
Color::Default => {
|
|
unreachable!();
|
|
}
|
|
c if c.as_byte() < 16 => {
|
|
rect = rect.set("class", format!("c{}", c.as_byte()).as_str());
|
|
}
|
|
c => {
|
|
let c = c.as_byte();
|
|
let (r, g, b) = XTERM_COLORS[c as usize];
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
rect = rect.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
}
|
|
rows_group = rows_group.add(rect);
|
|
}
|
|
if !text.is_empty() {
|
|
let text_length = text.grapheme_width();
|
|
for c in text.chars() {
|
|
match c {
|
|
'"' => escaped_text.push_str("""),
|
|
'&' => escaped_text.push_str("&"),
|
|
'\'' => escaped_text.push_str("'"),
|
|
'<' => escaped_text.push_str("<"),
|
|
'>' => escaped_text.push_str(">"),
|
|
c => escaped_text.push(c),
|
|
}
|
|
}
|
|
let mut text_el = Text::new()
|
|
.add(TextNode::new(&escaped_text))
|
|
.set("x", prev_x_fg * LETTER_WIDTH)
|
|
.set("textLength", text_length * LETTER_WIDTH);
|
|
/*.set("fgname", format!("{:?}", cur_fg));*/
|
|
if cur_attrs.intersects(Attr::BOLD) {
|
|
text_el = text_el.set("font-weight", "bold");
|
|
}
|
|
if cur_attrs.intersects(Attr::ITALICS) {
|
|
text_el = text_el.set("font-style", "italic");
|
|
}
|
|
if cur_attrs.intersects(Attr::UNDERLINE) {
|
|
text_el = text_el.set("text-decoration", "underline");
|
|
}
|
|
if cur_attrs.intersects(Attr::DIM) {
|
|
text_el = text_el.set("font-weight", "lighter");
|
|
}
|
|
if cur_attrs.intersects(Attr::HIDDEN) {
|
|
text_el = text_el.set("display", "none");
|
|
}
|
|
match cur_fg {
|
|
Color::Default if cur_attrs.intersects(Attr::REVERSE) => {
|
|
text_el = text_el.set("class", "b");
|
|
}
|
|
Color::Default => {
|
|
text_el = text_el.set("class", "f");
|
|
}
|
|
Color::Rgb(r, g, b) => {
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
text_el = text_el.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
c if c.as_byte() < 16 => {
|
|
text_el = text_el.set("class", format!("c{}", c.as_byte()).as_str());
|
|
}
|
|
c => {
|
|
let c = c.as_byte();
|
|
let (r, g, b) = XTERM_COLORS[c as usize];
|
|
let class = if classes.contains_key(&(r, g, b)) {
|
|
classes[&(r, g, b)]
|
|
} else {
|
|
let classes_size = classes.len();
|
|
classes.insert((r, g, b), classes_size);
|
|
classes_size
|
|
};
|
|
text_el = text_el.set("class", format!("f{:x}", class).as_str());
|
|
}
|
|
}
|
|
row_group = row_group.add(text_el);
|
|
text.clear();
|
|
escaped_text.clear();
|
|
}
|
|
definitions = definitions.add(row_group);
|
|
rows_group = rows_group.add(
|
|
Use::new()
|
|
.set("xlink:href", format!("#{:x}", row_idx + 1))
|
|
.set("y", LETTER_HEIGHT * row_idx),
|
|
);
|
|
}
|
|
let mut style_string = CSS_STYLE.to_string();
|
|
for ((r, g, b), name) in classes {
|
|
style_string
|
|
.extend(format!(".f{:x}{{fill:#{:02x}{:02x}{:02x};}}", name, r, g, b).chars());
|
|
}
|
|
let document = Document::new()
|
|
.set(
|
|
"viewBox",
|
|
(0, 0, width * LETTER_WIDTH, height * LETTER_HEIGHT + 1),
|
|
)
|
|
.set("width", width * LETTER_WIDTH)
|
|
.set("height", height * LETTER_HEIGHT + 1)
|
|
.add(Definitions::new().add(Style::new(&style_string).set("type", "text/css")))
|
|
.add(
|
|
Document::new()
|
|
.set("id", "t")
|
|
.set("preserveAspectRatio", "xMidYMin slice")
|
|
.set(
|
|
"viewBox",
|
|
(0, 0, width * LETTER_WIDTH, height * LETTER_HEIGHT),
|
|
)
|
|
.set("width", width * LETTER_WIDTH)
|
|
.set("height", height * LETTER_HEIGHT)
|
|
.add(
|
|
Rectangle::new()
|
|
.set("class", "b")
|
|
.set("height", "100%")
|
|
.set("width", "100%")
|
|
.set("x", 0)
|
|
.set("y", 0),
|
|
)
|
|
.add(definitions)
|
|
.add(rows_group),
|
|
)
|
|
.set("xmlns", "http://www.w3.org/2000/svg")
|
|
.set("baseProfile", "full")
|
|
.set("xmlns:xlink", "http://www.w3.org/1999/xlink")
|
|
.set("version", "1.1");
|
|
|
|
let mut s = Vec::new();
|
|
svg_crate::write(&mut s, &document).unwrap();
|
|
let mut res = Vec::new();
|
|
/*
|
|
* svg crate formats text nodes like this:
|
|
*
|
|
* <text>
|
|
* actual content
|
|
* </text>
|
|
*
|
|
* But we don't want any extra newlines before/after the tags:
|
|
*
|
|
* <text>actual content</text>
|
|
*
|
|
* So remove all new lines from SVG file.
|
|
*/
|
|
for b in s {
|
|
if b == b'\n' {
|
|
continue;
|
|
}
|
|
res.push(b);
|
|
}
|
|
let window = web_sys::window().expect("no global `window` exists");
|
|
let document = window.document().expect("should have a document on window");
|
|
|
|
// Manufacture the element we're gonna append
|
|
let val = if let Some(val) = document.get_element_by_id("terminal") {
|
|
val
|
|
} else {
|
|
js_console("COULD NOT GET ELEMENT BY ID #terminal !!!");
|
|
return;
|
|
};
|
|
val.set_inner_html(unsafe { std::str::from_utf8_unchecked(&res) });
|
|
}
|
|
|
|
/// Draw the entire screen from scratch.
|
|
pub fn render(&mut self) {
|
|
self.update_size();
|
|
let cols = self.cols;
|
|
let rows = self.rows;
|
|
self.context
|
|
.dirty_areas
|
|
.push_back(((0, 0), (cols - 1, rows - 1)));
|
|
|
|
self.redraw();
|
|
}
|
|
|
|
pub fn draw_component(&mut self, idx: usize) {
|
|
let component = &mut self.components[idx];
|
|
let upper_left = (0, 0);
|
|
let bottom_right = (self.cols - 1, self.rows - 1);
|
|
|
|
if component.is_dirty() {
|
|
component.draw(
|
|
&mut self.grid,
|
|
(upper_left, bottom_right),
|
|
&mut self.context,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn can_quit_cleanly(&mut self) -> bool {
|
|
let State {
|
|
ref mut components,
|
|
ref context,
|
|
..
|
|
} = self;
|
|
components.iter_mut().all(|c| c.can_quit_cleanly(context))
|
|
}
|
|
|
|
pub fn register_component(&mut self, component: Box<dyn Component>) {
|
|
self.components.push(component);
|
|
}
|
|
|
|
/// Convert user commands to actions/method calls.
|
|
fn exec_command(&mut self, cmd: Action) {
|
|
match cmd {
|
|
SetEnv(key, val) => {
|
|
env::set_var(key.as_str(), val.as_str());
|
|
}
|
|
PrintEnv(key) => {
|
|
self.context
|
|
.replies
|
|
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
|
|
env::var(key.as_str()).unwrap_or_else(|e| e.to_string()),
|
|
)));
|
|
}
|
|
Mailbox(account_name, op) => {
|
|
if let Some(account) = self
|
|
.context
|
|
.accounts
|
|
.values_mut()
|
|
.find(|a| a.name() == account_name)
|
|
{
|
|
if let Err(err) = account.mailbox_operation(op) {
|
|
self.context.replies.push_back(UIEvent::StatusEvent(
|
|
StatusEvent::DisplayMessage(err.to_string()),
|
|
));
|
|
}
|
|
} else {
|
|
self.context.replies.push_back(UIEvent::StatusEvent(
|
|
StatusEvent::DisplayMessage(format!(
|
|
"Account with name `{}` not found.",
|
|
account_name
|
|
)),
|
|
));
|
|
}
|
|
}
|
|
#[cfg(feature = "sqlite3")]
|
|
AccountAction(ref account_name, ReIndex) => {
|
|
let account_index = if let Some(a) = self
|
|
.context
|
|
.accounts
|
|
.iter()
|
|
.position(|(_, acc)| acc.name() == account_name)
|
|
{
|
|
a
|
|
} else {
|
|
self.context.replies.push_back(UIEvent::Notification(
|
|
None,
|
|
format!("Account {} was not found.", account_name),
|
|
Some(NotificationType::Error(ErrorKind::None)),
|
|
));
|
|
return;
|
|
};
|
|
if *self.context.accounts[account_index]
|
|
.settings
|
|
.conf
|
|
.search_backend()
|
|
!= crate::conf::SearchBackend::Sqlite3
|
|
{
|
|
self.context.replies.push_back(UIEvent::Notification(
|
|
None,
|
|
format!(
|
|
"Account {} doesn't have an sqlite3 search backend.",
|
|
account_name
|
|
),
|
|
Some(NotificationType::Error(ErrorKind::None)),
|
|
));
|
|
return;
|
|
}
|
|
match crate::sqlite3::index(&mut self.context, account_index) {
|
|
Ok(job) => {
|
|
let (channel, handle, job_id) =
|
|
self.context.job_executor.spawn_blocking(job);
|
|
self.context.accounts[account_index].active_jobs.insert(
|
|
job_id,
|
|
crate::conf::accounts::JobRequest::Generic {
|
|
name: "Message index rebuild".into(),
|
|
handle,
|
|
channel,
|
|
on_finish: None,
|
|
logging_level: melib::LoggingLevel::INFO,
|
|
},
|
|
);
|
|
self.context.replies.push_back(UIEvent::Notification(
|
|
None,
|
|
"Message index rebuild started.".to_string(),
|
|
Some(NotificationType::Info),
|
|
));
|
|
}
|
|
Err(err) => {
|
|
self.context.replies.push_back(UIEvent::Notification(
|
|
Some("Message index rebuild failed".to_string()),
|
|
err.to_string(),
|
|
Some(NotificationType::Error(err.kind)),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
#[cfg(not(feature = "sqlite3"))]
|
|
AccountAction(ref account_name, ReIndex) => {
|
|
self.context.replies.push_back(UIEvent::Notification(
|
|
None,
|
|
"Message index rebuild failed: meli is not built with sqlite3 support."
|
|
.to_string(),
|
|
Some(NotificationType::Error(ErrorKind::None)),
|
|
));
|
|
}
|
|
AccountAction(ref account_name, PrintAccountSetting(ref setting)) => {
|
|
let path = setting.split(".").collect::<SmallVec<[&str; 16]>>();
|
|
if let Some(pos) = self
|
|
.context
|
|
.accounts
|
|
.iter()
|
|
.position(|(_h, a)| a.name() == account_name)
|
|
{
|
|
self.context.replies.push_back(UIEvent::StatusEvent(
|
|
StatusEvent::UpdateStatus(format!(
|
|
"{}",
|
|
self.context.accounts[pos]
|
|
.settings
|
|
.lookup("settings", &path)
|
|
.unwrap_or_else(|err| err.to_string())
|
|
)),
|
|
));
|
|
} else {
|
|
self.context.replies.push_back(UIEvent::Notification(
|
|
None,
|
|
format!("Account {} was not found.", account_name),
|
|
Some(NotificationType::Error(ErrorKind::None)),
|
|
));
|
|
return;
|
|
}
|
|
}
|
|
PrintSetting(ref setting) => {
|
|
let path = setting.split(".").collect::<SmallVec<[&str; 16]>>();
|
|
self.context
|
|
.replies
|
|
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(format!(
|
|
"{}",
|
|
self.context
|
|
.settings
|
|
.lookup("settings", &path)
|
|
.unwrap_or_else(|err| err.to_string())
|
|
))));
|
|
}
|
|
v => {
|
|
self.rcv_event(UIEvent::Action(v));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The application's main loop sends `UIEvents` to state via this method.
|
|
pub fn rcv_event(&mut self, mut event: UIEvent) {
|
|
js_console(&format!("got event: {:?}", &event));
|
|
if let UIEvent::Input(_) = event {
|
|
if self.display_messages_expiration_start.is_none() {
|
|
self.display_messages_expiration_start = Some(melib::datetime::now());
|
|
}
|
|
}
|
|
|
|
match event {
|
|
// Command type is handled only by State.
|
|
UIEvent::Command(cmd) => {
|
|
if let Ok(action) = parse_command(&cmd.as_bytes()) {
|
|
if action.needs_confirmation() {
|
|
self.overlay.push(Box::new(UIConfirmationDialog::new(
|
|
"You sure?",
|
|
vec![(true, "yes".to_string()), (false, "no".to_string())],
|
|
true,
|
|
Some(Box::new(move |id: ComponentId, result: bool| {
|
|
Some(UIEvent::FinishedUIDialog(
|
|
id,
|
|
Box::new(if result { Some(action) } else { None }),
|
|
))
|
|
})),
|
|
&mut self.context,
|
|
)));
|
|
} else {
|
|
self.exec_command(action);
|
|
}
|
|
} else {
|
|
self.context.replies.push_back(UIEvent::StatusEvent(
|
|
StatusEvent::DisplayMessage("invalid command".to_string()),
|
|
));
|
|
}
|
|
return;
|
|
}
|
|
UIEvent::Fork(ForkType::Finished) => {
|
|
/*
|
|
* Fork has finished in the past.
|
|
* We're back in the AlternateScreen, but the cursor is reset to Shown, so fix
|
|
* it.
|
|
write!(self.stdout(), "{}", cursor::Hide,).unwrap();
|
|
self.flush();
|
|
*/
|
|
self.switch_to_main_screen();
|
|
self.switch_to_alternate_screen();
|
|
self.context.restore_input();
|
|
return;
|
|
}
|
|
UIEvent::Fork(ForkType::Generic(child)) => {
|
|
self.context.children.push(child);
|
|
return;
|
|
}
|
|
UIEvent::Fork(child) => {
|
|
self.mode = UIMode::Fork;
|
|
self.child = Some(child);
|
|
return;
|
|
}
|
|
UIEvent::BackendEvent(
|
|
account_hash,
|
|
BackendEvent::Notice {
|
|
ref description,
|
|
ref content,
|
|
level,
|
|
},
|
|
) => {
|
|
log(
|
|
format!(
|
|
"{}: {}{}{}",
|
|
self.context.accounts[&account_hash].name(),
|
|
description.as_ref().map(|s| s.as_str()).unwrap_or(""),
|
|
if description.is_some() { ": " } else { "" },
|
|
content.as_str()
|
|
),
|
|
level,
|
|
);
|
|
self.rcv_event(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
|
|
content.to_string(),
|
|
)));
|
|
return;
|
|
}
|
|
UIEvent::BackendEvent(_, BackendEvent::Refresh(refresh_event)) => {
|
|
self.refresh_event(refresh_event);
|
|
return;
|
|
}
|
|
UIEvent::ChangeMode(m) => {
|
|
self.context
|
|
.sender
|
|
.send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m)))
|
|
.unwrap();
|
|
}
|
|
UIEvent::Timer(id) if id == self.draw_rate_limit.id() => {
|
|
self.draw_rate_limit.reset();
|
|
self.redraw();
|
|
return;
|
|
}
|
|
UIEvent::Input(Key::Alt('<')) => {
|
|
self.display_messages_expiration_start = Some(melib::datetime::now());
|
|
self.display_messages_active = true;
|
|
self.display_messages_pos = self.display_messages_pos.saturating_sub(1);
|
|
return;
|
|
}
|
|
UIEvent::Input(Key::Alt('>')) => {
|
|
self.display_messages_expiration_start = Some(melib::datetime::now());
|
|
self.display_messages_active = true;
|
|
self.display_messages_pos = std::cmp::min(
|
|
self.display_messages.len().saturating_sub(1),
|
|
self.display_messages_pos + 1,
|
|
);
|
|
return;
|
|
}
|
|
UIEvent::StatusEvent(StatusEvent::DisplayMessage(ref msg)) => {
|
|
self.display_messages.push(DisplayMessage {
|
|
timestamp: melib::datetime::now(),
|
|
msg: msg.clone(),
|
|
});
|
|
self.display_messages_active = true;
|
|
self.display_messages_expiration_start = None;
|
|
self.display_messages_pos = self.display_messages.len() - 1;
|
|
self.redraw();
|
|
}
|
|
UIEvent::FinishedUIDialog(ref id, ref mut results)
|
|
if self.overlay.iter().any(|c| c.id() == *id) =>
|
|
{
|
|
if let Some(Some(ref mut action)) = results.downcast_mut::<Option<Action>>() {
|
|
self.exec_command(std::mem::replace(action, Action::ToggleThreadSnooze));
|
|
|
|
let pos = self.overlay.iter().position(|c| c.id() == *id).unwrap();
|
|
self.overlay.remove(pos);
|
|
return;
|
|
}
|
|
}
|
|
UIEvent::Callback(callback_fn) => {
|
|
(callback_fn.0)(&mut self.context);
|
|
return;
|
|
}
|
|
UIEvent::GlobalUIDialog(dialog) => {
|
|
self.overlay.push(dialog);
|
|
return;
|
|
}
|
|
UIEvent::RefreshEvent(ev) => {
|
|
self.refresh_event(*ev);
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
let Self {
|
|
ref mut components,
|
|
ref mut context,
|
|
ref mut overlay,
|
|
..
|
|
} = self;
|
|
|
|
/* inform each component */
|
|
for c in overlay.iter_mut().chain(components.iter_mut()) {
|
|
if c.process_event(&mut event, context) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if !self.context.replies.is_empty() {
|
|
let replies: smallvec::SmallVec<[UIEvent; 8]> =
|
|
self.context.replies.drain(0..).collect();
|
|
// Pass replies to self and call count on the map iterator to force evaluation
|
|
replies.into_iter().map(|r| self.rcv_event(r)).count();
|
|
}
|
|
}
|
|
|
|
pub fn try_wait_on_child(&mut self) -> Option<bool> {
|
|
None
|
|
/*
|
|
let should_return_flag = match self.child {
|
|
Some(ForkType::NewDraft(_, ref mut c)) => {
|
|
let w = c.try_wait();
|
|
match w {
|
|
Ok(Some(_)) => true,
|
|
Ok(None) => false,
|
|
Err(e) => {
|
|
log(
|
|
format!("Failed to wait on editor process: {}", e.to_string()),
|
|
ERROR,
|
|
);
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
Some(ForkType::Generic(ref mut c)) => {
|
|
let w = c.try_wait();
|
|
match w {
|
|
Ok(Some(_)) => true,
|
|
Ok(None) => false,
|
|
Err(e) => {
|
|
log(
|
|
format!("Failed to wait on child process: {}", e.to_string()),
|
|
ERROR,
|
|
);
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
Some(ForkType::Finished) => {
|
|
/* Fork has already finished */
|
|
self.child = None;
|
|
return None;
|
|
}
|
|
_ => {
|
|
return None;
|
|
}
|
|
};
|
|
if should_return_flag {
|
|
return Some(true);
|
|
}
|
|
Some(false)
|
|
*/
|
|
}
|
|
fn flush(&mut self) {}
|
|
pub fn check_accounts(&mut self) {
|
|
let mut ctr = 0;
|
|
for i in 0..self.context.accounts.len() {
|
|
if self.context.is_online_idx(i).is_ok() {
|
|
ctr += 1;
|
|
}
|
|
}
|
|
if ctr != self.context.accounts.len() {
|
|
//self.timer.thread().unpark();
|
|
}
|
|
//self.context.input_thread.check();
|
|
}
|
|
}
|
|
const CSS_STYLE: &'static str = r#"#t{font-family:var(--terminal-font);font-style:normal;font-size:14px;}text{dominant-baseline:text-before-edge;white-space:pre;transform:translateY(-2px);}.f{fill:#e5e5e5;}.b{fill:#000;}.c0{fill:#000;}.c1{fill:#cd0000;}.c2{fill:#00cd00;}.c3{fill:#cdcd00;}.c4{fill:#00e;}.c5{fill:#cd00cd;}.c6{fill:#00cdcd;}.c7{fill:#e5e5e5;}.c8{fill:#7f7f7f;}.c9{fill:#f00;}.c10{fill:#0f0;}.c11{fill:#ff0;}.c12{fill:#5c5cff;}.c13{fill:#f0f;}.c14{fill:#0ff;}.c15{fill:#fff;}"#;
|
|
|
|
const XTERM_COLORS: &'static [(u8, u8, u8)] = &[
|
|
/*0*/ (0, 0, 0),
|
|
/*1*/ (128, 0, 0),
|
|
/*2*/ (0, 128, 0),
|
|
/*3*/ (128, 128, 0),
|
|
/*4*/ (0, 0, 128),
|
|
/*5*/ (128, 0, 128),
|
|
/*6*/ (0, 128, 128),
|
|
/*7*/ (192, 192, 192),
|
|
/*8*/ (128, 128, 128),
|
|
/*9*/ (255, 0, 0),
|
|
/*10*/ (0, 255, 0),
|
|
/*11*/ (255, 255, 0),
|
|
/*12*/ (0, 0, 255),
|
|
/*13*/ (255, 0, 255),
|
|
/*14*/ (0, 255, 255),
|
|
/*15*/ (255, 255, 255),
|
|
/*16*/ (0, 0, 0),
|
|
/*17*/ (0, 0, 95),
|
|
/*18*/ (0, 0, 135),
|
|
/*19*/ (0, 0, 175),
|
|
/*20*/ (0, 0, 215),
|
|
/*21*/ (0, 0, 255),
|
|
/*22*/ (0, 95, 0),
|
|
/*23*/ (0, 95, 95),
|
|
/*24*/ (0, 95, 135),
|
|
/*25*/ (0, 95, 175),
|
|
/*26*/ (0, 95, 215),
|
|
/*27*/ (0, 95, 255),
|
|
/*28*/ (0, 135, 0),
|
|
/*29*/ (0, 135, 95),
|
|
/*30*/ (0, 135, 135),
|
|
/*31*/ (0, 135, 175),
|
|
/*32*/ (0, 135, 215),
|
|
/*33*/ (0, 135, 255),
|
|
/*34*/ (0, 175, 0),
|
|
/*35*/ (0, 175, 95),
|
|
/*36*/ (0, 175, 135),
|
|
/*37*/ (0, 175, 175),
|
|
/*38*/ (0, 175, 215),
|
|
/*39*/ (0, 175, 255),
|
|
/*40*/ (0, 215, 0),
|
|
/*41*/ (0, 215, 95),
|
|
/*42*/ (0, 215, 135),
|
|
/*43*/ (0, 215, 175),
|
|
/*44*/ (0, 215, 215),
|
|
/*45*/ (0, 215, 255),
|
|
/*46*/ (0, 255, 0),
|
|
/*47*/ (0, 255, 95),
|
|
/*48*/ (0, 255, 135),
|
|
/*49*/ (0, 255, 175),
|
|
/*50*/ (0, 255, 215),
|
|
/*51*/ (0, 255, 255),
|
|
/*52*/ (95, 0, 0),
|
|
/*53*/ (95, 0, 95),
|
|
/*54*/ (95, 0, 135),
|
|
/*55*/ (95, 0, 175),
|
|
/*56*/ (95, 0, 215),
|
|
/*57*/ (95, 0, 255),
|
|
/*58*/ (95, 95, 0),
|
|
/*59*/ (95, 95, 95),
|
|
/*60*/ (95, 95, 135),
|
|
/*61*/ (95, 95, 175),
|
|
/*62*/ (95, 95, 215),
|
|
/*63*/ (95, 95, 255),
|
|
/*64*/ (95, 135, 0),
|
|
/*65*/ (95, 135, 95),
|
|
/*66*/ (95, 135, 135),
|
|
/*67*/ (95, 135, 175),
|
|
/*68*/ (95, 135, 215),
|
|
/*69*/ (95, 135, 255),
|
|
/*70*/ (95, 175, 0),
|
|
/*71*/ (95, 175, 95),
|
|
/*72*/ (95, 175, 135),
|
|
/*73*/ (95, 175, 175),
|
|
/*74*/ (95, 175, 215),
|
|
/*75*/ (95, 175, 255),
|
|
/*76*/ (95, 215, 0),
|
|
/*77*/ (95, 215, 95),
|
|
/*78*/ (95, 215, 135),
|
|
/*79*/ (95, 215, 175),
|
|
/*80*/ (95, 215, 215),
|
|
/*81*/ (95, 215, 255),
|
|
/*82*/ (95, 255, 0),
|
|
/*83*/ (95, 255, 95),
|
|
/*84*/ (95, 255, 135),
|
|
/*85*/ (95, 255, 175),
|
|
/*86*/ (95, 255, 215),
|
|
/*87*/ (95, 255, 255),
|
|
/*88*/ (135, 0, 0),
|
|
/*89*/ (135, 0, 95),
|
|
/*90*/ (135, 0, 135),
|
|
/*91*/ (135, 0, 175),
|
|
/*92*/ (135, 0, 215),
|
|
/*93*/ (135, 0, 255),
|
|
/*94*/ (135, 95, 0),
|
|
/*95*/ (135, 95, 95),
|
|
/*96*/ (135, 95, 135),
|
|
/*97*/ (135, 95, 175),
|
|
/*98*/ (135, 95, 215),
|
|
/*99*/ (135, 95, 255),
|
|
/*100*/ (135, 135, 0),
|
|
/*101*/ (135, 135, 95),
|
|
/*102*/ (135, 135, 135),
|
|
/*103*/ (135, 135, 175),
|
|
/*104*/ (135, 135, 215),
|
|
/*105*/ (135, 135, 255),
|
|
/*106*/ (135, 175, 0),
|
|
/*107*/ (135, 175, 95),
|
|
/*108*/ (135, 175, 135),
|
|
/*109*/ (135, 175, 175),
|
|
/*110*/ (135, 175, 215),
|
|
/*111*/ (135, 175, 255),
|
|
/*112*/ (135, 215, 0),
|
|
/*113*/ (135, 215, 95),
|
|
/*114*/ (135, 215, 135),
|
|
/*115*/ (135, 215, 175),
|
|
/*116*/ (135, 215, 215),
|
|
/*117*/ (135, 215, 255),
|
|
/*118*/ (135, 255, 0),
|
|
/*119*/ (135, 255, 95),
|
|
/*120*/ (135, 255, 135),
|
|
/*121*/ (135, 255, 175),
|
|
/*122*/ (135, 255, 215),
|
|
/*123*/ (135, 255, 255),
|
|
/*124*/ (175, 0, 0),
|
|
/*125*/ (175, 0, 95),
|
|
/*126*/ (175, 0, 135),
|
|
/*127*/ (175, 0, 175),
|
|
/*128*/ (175, 0, 215),
|
|
/*129*/ (175, 0, 255),
|
|
/*130*/ (175, 95, 0),
|
|
/*131*/ (175, 95, 95),
|
|
/*132*/ (175, 95, 135),
|
|
/*133*/ (175, 95, 175),
|
|
/*134*/ (175, 95, 215),
|
|
/*135*/ (175, 95, 255),
|
|
/*136*/ (175, 135, 0),
|
|
/*137*/ (175, 135, 95),
|
|
/*138*/ (175, 135, 135),
|
|
/*139*/ (175, 135, 175),
|
|
/*140*/ (175, 135, 215),
|
|
/*141*/ (175, 135, 255),
|
|
/*142*/ (175, 175, 0),
|
|
/*143*/ (175, 175, 95),
|
|
/*144*/ (175, 175, 135),
|
|
/*145*/ (175, 175, 175),
|
|
/*146*/ (175, 175, 215),
|
|
/*147*/ (175, 175, 255),
|
|
/*148*/ (175, 215, 0),
|
|
/*149*/ (175, 215, 95),
|
|
/*150*/ (175, 215, 135),
|
|
/*151*/ (175, 215, 175),
|
|
/*152*/ (175, 215, 215),
|
|
/*153*/ (175, 215, 255),
|
|
/*154*/ (175, 255, 0),
|
|
/*155*/ (175, 255, 95),
|
|
/*156*/ (175, 255, 135),
|
|
/*157*/ (175, 255, 175),
|
|
/*158*/ (175, 255, 215),
|
|
/*159*/ (175, 255, 255),
|
|
/*160*/ (215, 0, 0),
|
|
/*161*/ (215, 0, 95),
|
|
/*162*/ (215, 0, 135),
|
|
/*163*/ (215, 0, 175),
|
|
/*164*/ (215, 0, 215),
|
|
/*165*/ (215, 0, 255),
|
|
/*166*/ (215, 95, 0),
|
|
/*167*/ (215, 95, 95),
|
|
/*168*/ (215, 95, 135),
|
|
/*169*/ (215, 95, 175),
|
|
/*170*/ (215, 95, 215),
|
|
/*171*/ (215, 95, 255),
|
|
/*172*/ (215, 135, 0),
|
|
/*173*/ (215, 135, 95),
|
|
/*174*/ (215, 135, 135),
|
|
/*175*/ (215, 135, 175),
|
|
/*176*/ (215, 135, 215),
|
|
/*177*/ (215, 135, 255),
|
|
/*178*/ (215, 175, 0),
|
|
/*179*/ (215, 175, 95),
|
|
/*180*/ (215, 175, 135),
|
|
/*181*/ (215, 175, 175),
|
|
/*182*/ (215, 175, 215),
|
|
/*183*/ (215, 175, 255),
|
|
/*184*/ (215, 215, 0),
|
|
/*185*/ (215, 215, 95),
|
|
/*186*/ (215, 215, 135),
|
|
/*187*/ (215, 215, 175),
|
|
/*188*/ (215, 215, 215),
|
|
/*189*/ (215, 215, 255),
|
|
/*190*/ (215, 255, 0),
|
|
/*191*/ (215, 255, 95),
|
|
/*192*/ (215, 255, 135),
|
|
/*193*/ (215, 255, 175),
|
|
/*194*/ (215, 255, 215),
|
|
/*195*/ (215, 255, 255),
|
|
/*196*/ (255, 0, 0),
|
|
/*197*/ (255, 0, 95),
|
|
/*198*/ (255, 0, 135),
|
|
/*199*/ (255, 0, 175),
|
|
/*200*/ (255, 0, 215),
|
|
/*201*/ (255, 0, 255),
|
|
/*202*/ (255, 95, 0),
|
|
/*203*/ (255, 95, 95),
|
|
/*204*/ (255, 95, 135),
|
|
/*205*/ (255, 95, 175),
|
|
/*206*/ (255, 95, 215),
|
|
/*207*/ (255, 95, 255),
|
|
/*208*/ (255, 135, 0),
|
|
/*209*/ (255, 135, 95),
|
|
/*210*/ (255, 135, 135),
|
|
/*211*/ (255, 135, 175),
|
|
/*212*/ (255, 135, 215),
|
|
/*213*/ (255, 135, 255),
|
|
/*214*/ (255, 175, 0),
|
|
/*215*/ (255, 175, 95),
|
|
/*216*/ (255, 175, 135),
|
|
/*217*/ (255, 175, 175),
|
|
/*218*/ (255, 175, 215),
|
|
/*219*/ (255, 175, 255),
|
|
/*220*/ (255, 215, 0),
|
|
/*221*/ (255, 215, 95),
|
|
/*222*/ (255, 215, 135),
|
|
/*223*/ (255, 215, 175),
|
|
/*224*/ (255, 215, 215),
|
|
/*225*/ (255, 215, 255),
|
|
/*226*/ (255, 255, 0),
|
|
/*227*/ (255, 255, 95),
|
|
/*228*/ (255, 255, 135),
|
|
/*229*/ (255, 255, 175),
|
|
/*230*/ (255, 255, 215),
|
|
/*231*/ (255, 255, 255),
|
|
/*232*/ (8, 8, 8),
|
|
/*233*/ (18, 18, 18),
|
|
/*234*/ (28, 28, 28),
|
|
/*235*/ (38, 38, 38),
|
|
/*236*/ (48, 48, 48),
|
|
/*237*/ (58, 58, 58),
|
|
/*238*/ (68, 68, 68),
|
|
/*239*/ (78, 78, 78),
|
|
/*240*/ (88, 88, 88),
|
|
/*241*/ (98, 98, 98),
|
|
/*242*/ (108, 108, 108),
|
|
/*243*/ (118, 118, 118),
|
|
/*244*/ (128, 128, 128),
|
|
/*245*/ (138, 138, 138),
|
|
/*246*/ (148, 148, 148),
|
|
/*247*/ (158, 158, 158),
|
|
/*248*/ (168, 168, 168),
|
|
/*249*/ (178, 178, 178),
|
|
/*250*/ (188, 188, 188),
|
|
/*251*/ (198, 198, 198),
|
|
/*252*/ (208, 208, 208),
|
|
/*253*/ (218, 218, 218),
|
|
/*254*/ (228, 228, 228),
|
|
/*255*/ (238, 238, 238),
|
|
];
|