use termion::event::Key; use failure::Fail; use chrono::{DateTime, Local}; use crate::term; use crate::widget::Widget; use crate::listview::{ListView, Listable}; use crate::fail::{HResult, HError}; use crate::dirty::Dirtyable; pub type LogView = ListView>; #[derive(Debug)] pub struct LogEntry { description: String, content: Option, lines: usize, folded: bool } impl Foldable for LogEntry { fn description(&self) -> &str { &self.description } fn content(&self) -> Option<&String> { self.content.as_ref() } fn lines(&self) -> usize { if self.is_folded() { 1 } else { self.lines } } fn toggle_fold(&mut self) { self.folded = !self.folded; } fn is_folded(&self) -> bool { self.folded } } impl From<&HError> for LogEntry { fn from(from: &HError) -> LogEntry { let time: DateTime = Local::now(); let logcolor = match from { HError::Log(_) => term::normal_color(), _ => term::color_red() }; let description = format!("{}{}{}: {}", term::color_green(), time.format("%F %R"), logcolor, from).lines().take(1).collect(); let mut content = format!("{}{}{}: {}\n", term::color_green(), time.format("%F %R"), logcolor, from); if let Some(cause) = from.cause() { content += &format!("{}\n", cause); } if let Some(backtrace) = from.backtrace() { content += &format!("{}\n", backtrace); } let lines = content.lines().count(); LogEntry { description: description, content: Some(content), lines: lines, folded: true } } } pub trait FoldableWidgetExt { fn on_refresh(&mut self) -> HResult<()> { Ok(()) } fn render_header(&self) -> HResult { Ok("".to_string()) } fn render_footer(&self) -> HResult { Ok("".to_string()) } fn on_key(&mut self, key: Key) -> HResult<()> { HError::undefined_key(key)? } fn render(&self) -> Vec { vec![] } } impl FoldableWidgetExt for ListView> { fn on_refresh(&mut self) -> HResult<()> { if self.content.refresh_logs()? > 0 { self.core.set_dirty(); } Ok(()) } fn render_header(&self) -> HResult { let (xsize, _) = self.core.coordinates.size_u(); let current = self.current_fold().map(|n| n+1).unwrap_or(0); let num = self.content.len(); let hint = format!("{} / {}", current, num); let hint_xpos = xsize - hint.len(); let header = format!("Logged entries: {}{}{}", num, term::goto_xy_u(hint_xpos, 0), hint); Ok(header) } fn render_footer(&self) -> HResult { let current = self.current_fold()?; if let Some(logentry) = self.content.get(current) { let (xsize, ysize) = self.core.coordinates.size_u(); let (_, ypos) = self.core.coordinates.position_u(); let description = logentry.description(); let lines = logentry.lines(); let start_pos = self.fold_start_pos(current); let selection = self.get_selection(); let current_line = (selection - start_pos) + 1; let line_hint = format!("{} / {}", current_line, lines); let hint_xpos = xsize - line_hint.len(); let hint_ypos = ysize + ypos + 1; let sized_description = term::sized_string_u(&description, xsize - (line_hint.len()+2)); let footer = format!("{}{}{}{}{}", sized_description, term::reset(), term::status_bg(), term::goto_xy_u(hint_xpos, hint_ypos), line_hint); Ok(footer) } else { Ok("No log entries".to_string()) } } } trait LogList { fn refresh_logs(&mut self) -> HResult; } impl LogList for Vec { fn refresh_logs(&mut self) -> HResult { let logs = crate::fail::get_logs()?; let mut logentries = logs.into_iter().map(|log| { LogEntry::from(log) }).collect::>(); let n = logentries.len(); self.append(&mut logentries); Ok(n) } } pub trait Foldable { fn description(&self) -> &str; fn content(&self) -> Option<&String>; fn lines(&self) -> usize; fn toggle_fold(&mut self); fn is_folded(&self) -> bool; fn text(&self) -> &str { if !self.is_folded() && self.content().is_some() { self.content().unwrap() } else { &self.description() } } fn render_description(&self) -> String { self.description().to_string() } fn render_content(&self) -> Vec { if let Some(content) = self.content() { content .lines() .map(|line| line.to_string()) .collect() } else { vec![self.render_description()] } } fn render(&self) -> Vec { if self.is_folded() { vec![self.render_description()] } else { self.render_content() } } } impl ListView> where ListView>: FoldableWidgetExt { pub fn toggle_fold(&mut self) -> HResult<()> { let fold = self.current_fold()?; let fold_pos = self.fold_start_pos(fold); self.content[fold].toggle_fold(); if self.content[fold].is_folded() { self.set_selection(fold_pos); } self.core.set_dirty(); Ok(()) } pub fn fold_start_pos(&self, fold: usize) -> usize { self.content .iter() .take(fold) .fold(0, |pos, foldable| { pos + (foldable.lines()) }) } pub fn current_fold(&self) -> Option { let pos = self.get_selection(); let fold_lines = self .content .iter() .map(|f| f.lines()) .collect::>(); fold_lines .iter() .enumerate() .fold((0, None), |(lines, fold_pos), (i, current_fold_lines)| { if fold_pos.is_some() { (lines, fold_pos) } else { if lines + current_fold_lines > pos { (lines, Some(i)) } else { (lines + current_fold_lines, None) } }}).1 } } impl Listable for ListView> where ListView>: FoldableWidgetExt { fn len(&self) -> usize { self.content.iter().map(|f| f.lines()).sum() } fn render(&self) -> Vec { let rendering = FoldableWidgetExt::render(self); // HACK to check if no custom renderer if rendering.len() > 0 { return rendering; } let (xsize, _) = self.core.coordinates.size_u(); self.content .iter() .map(|foldable| foldable .render() .iter() .map(|line| term::sized_string_u(line, xsize)) .collect::>()) .flatten() .collect() } fn render_header(&self) -> HResult { FoldableWidgetExt::render_header(self) } fn render_footer(&self) -> HResult { FoldableWidgetExt::render_footer(self) } fn on_refresh(&mut self) -> HResult<()> { FoldableWidgetExt::on_refresh(self) } fn on_key(&mut self, key: Key) -> HResult<()> { let result = FoldableWidgetExt::on_key(self, key); if let Err(HError::WidgetUndefinedKeyError{key}) = result { match key { Key::Up | Key::Char('p') => self.move_up(), Key::Char('P') => for _ in 0..10 { self.move_up() }, Key::Char('N') => for _ in 0..10 { self.move_down() }, Key::Down | Key::Char('n') => self.move_down(), Key::Char('t') => self.toggle_fold()?, Key::Char('l') => self.popup_finnished()?, _ => { HError::undefined_key(key)?; }, } } result } }