diff --git a/css/html.css b/css/html.css new file mode 100644 index 0000000..d53ee47 --- /dev/null +++ b/css/html.css @@ -0,0 +1,97 @@ +html > head { + display: none; +} + +a { + color: #444; +} + +i, em, cite, var, address { + font-style: italic; +} + +b, strong { + font-weight: bold; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.75em 0; +} + +h3 { + font-size: 1.17em; + margin: 0.83em 0; +} + +h4, p, blockquote, dl { + margin: 1.12em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.5em 0; +} + +h6 { + font-size: 0.75em; + margin: 1.67em 0; +} + +dt { + margin-top: 1.12em; +} + +dd { + margin-left: 1.5em; +} + +pre, code, samp, kbd { + font-family: monospace; + font-size: 0.891em; +} + +ul, ol { + margin-left: 1.5em; + margin-top: 0.6rem; + margin-bottom: 0.6rem; +} + +li > ul, li > ol { + margin-top: 0; + margin-bottom: 0; +} + +svg { + text-align: center; +} + +sub, sup { + font-size: 0.83em; +} + +sub { + vertical-align: sub; +} + +sup { + vertical-align: super; +} + +table { + text-align: left; +} + +th { + font-weight: bold; + text-align: center; +} + +th, td { + padding: 0.67em; +} diff --git a/css/sysinfo.css b/css/sysinfo.css new file mode 100644 index 0000000..586c975 --- /dev/null +++ b/css/sysinfo.css @@ -0,0 +1,21 @@ +body { + font-family: sans-serif; + font-size: 8.1875pt; + text-align: center; +} + +table { + display: inline-table; +} + +tr { + padding-bottom: 0.5em; +} + +tr.sep { + padding-top: 0.5em; +} + +td { + padding: 0 0.5em 0; +} diff --git a/css/toc.css b/css/toc.css index b572a49..9a11419 100644 --- a/css/toc.css +++ b/css/toc.css @@ -3,16 +3,16 @@ ul { padding: 0; } -ul ul { +ul > ul { padding-left: 2em; } -ul li { +ul > li { list-style-type: none; padding-top: 0.5em; } -body > ul > li { +ul.top > li { padding-top: 1em; } diff --git a/src/app.rs b/src/app.rs index f721fef..1fb00f2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,6 +25,7 @@ use crate::view::keyboard::{Layout}; use crate::view::dictionary::Dictionary as DictionaryApp; use crate::view::calculator::Calculator; use crate::view::sketch::Sketch; +use crate::document::sys_info_as_html; use crate::input::{DeviceEvent, PowerSource, ButtonCode, ButtonStatus, VAL_RELEASE, VAL_PRESS}; use crate::input::{raw_events, device_events, usb_events, display_rotate_event, button_scheme_event}; use crate::gesture::{GestureEvent, gesture_events}; @@ -857,6 +858,19 @@ pub fn run() -> Result<(), Error> { rq.add(RenderData::new(dialog.id(), *dialog.rect(), UpdateMode::Gui)); view.children_mut().push(Box::new(dialog) as Box); }, + Event::Select(EntryId::SystemInfo) => { + view.children_mut().retain(|child| !child.is::()); + let html = sys_info_as_html(); + let r = Reader::from_html(context.fb.rect(), &html, &tx, &mut context); + let mut next_view = Box::new(r) as Box; + transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); + history.push(HistoryItem { + view, + rotation: context.display.rotation, + monochrome: context.fb.monochrome(), + }); + view = next_view; + }, Event::Select(EntryId::Launch(app_cmd)) => { view.children_mut().retain(|child| !child.is::()); let monochrome = context.fb.monochrome(); @@ -995,7 +1009,7 @@ pub fn run() -> Result<(), Error> { Event::Select(EntryId::TakeScreenshot) => { let name = Local::now().format("screenshot-%Y%m%d_%H%M%S.png"); let msg = match context.fb.save(&name.to_string()) { - Err(e) => format!("Couldn't take screenshot: {}).", e), + Err(e) => format!("{}", e), Ok(_) => format!("Saved {}.", name), }; let notif = Notification::new(ViewId::TakeScreenshotNotif, diff --git a/src/document/html/mod.rs b/src/document/html/mod.rs index 573ead3..cbd8ac2 100644 --- a/src/document/html/mod.rs +++ b/src/document/html/mod.rs @@ -6,7 +6,7 @@ pub mod style; pub mod layout; pub mod engine; -use std::io::Read; +use std::io::{Read, Write}; use std::fs::{self, File}; use std::path::{Path, PathBuf}; use fxhash::FxHashMap; @@ -29,6 +29,7 @@ const USER_STYLESHEET: &str = "css/html-user.css"; type UriCache = FxHashMap; pub struct HtmlDocument { + text: String, content: Node, engine: Engine, pages: Vec, @@ -55,13 +56,14 @@ impl HtmlDocument { pub fn new>(path: P) -> Result { let mut file = File::open(&path)?; let size = file.metadata()?.len() as usize; - let mut content = String::new(); - file.read_to_string(&mut content)?; - let mut content = XmlParser::new(&content).parse(); + let mut text = String::new(); + file.read_to_string(&mut text)?; + let mut content = XmlParser::new(&text).parse(); content.wrap_lost_inlines(); let parent = path.as_ref().parent().unwrap_or_else(|| Path::new("")); Ok(HtmlDocument { + text, content, engine: Engine::new(), pages: Vec::new(), @@ -73,12 +75,13 @@ impl HtmlDocument { }) } - pub fn new_from_memory(content: &str) -> HtmlDocument { - let size = content.len(); - let mut content = XmlParser::new(content).parse(); + pub fn new_from_memory(text: &str) -> HtmlDocument { + let size = text.len(); + let mut content = XmlParser::new(text).parse(); content.wrap_lost_inlines(); HtmlDocument { + text: text.to_string(), content, engine: Engine::new(), pages: Vec::new(), @@ -90,10 +93,11 @@ impl HtmlDocument { } } - pub fn update(&mut self, content: &str) { - self.size = content.len(); - self.content = XmlParser::new(content).parse(); + pub fn update(&mut self, text: &str) { + self.size = text.len(); + self.content = XmlParser::new(text).parse(); self.content.wrap_lost_inlines(); + self.text = text.to_string(); self.pages.clear(); } @@ -420,6 +424,12 @@ impl Document for HtmlDocument { .and_then(|child| child.attr("content").map(|s| decode_entities(s).into_owned())) } + fn save(&self, path: &str) -> Result<(), Error> { + let mut file = File::create(path)?; + file.write_all(self.text.as_bytes()) + .map_err(Into::into) + } + fn is_reflowable(&self) -> bool { true } diff --git a/src/document/mod.rs b/src/document/mod.rs index 472a5ec..f62accc 100644 --- a/src/document/mod.rs +++ b/src/document/mod.rs @@ -6,9 +6,16 @@ pub mod html; mod djvulibre_sys; mod mupdf_sys; +use std::env; +use std::process::Command; use std::path::Path; use std::ffi::OsStr; -use fxhash::FxHashSet; +use anyhow::{Error, format_err}; +use regex::Regex; +use nix::sys::statvfs; +#[cfg(target_os = "linux")] +use nix::sys::sysinfo; +use fxhash::{FxHashMap, FxHashSet}; use lazy_static::lazy_static; use unicode_normalization::UnicodeNormalization; use unicode_normalization::char::{is_combining_mark}; @@ -16,9 +23,12 @@ use serde::{Serialize, Deserialize}; use self::djvu::DjvuOpener; use self::pdf::PdfOpener; use self::epub::EpubDocument; +use self::html::HtmlDocument; use crate::geom::{Boundary, CycleDir}; use crate::metadata::{TextAlign}; use crate::framebuffer::Pixmap; +use crate::settings::INTERNAL_CARD_ROOT; +use crate::device::CURRENT_DEVICE; pub const BYTES_PER_PAGE: f64 = 2048.0; @@ -105,6 +115,10 @@ pub trait Document: Send+Sync { false } + fn save(&self, _path: &str) -> Result<(), Error> { + Err(format_err!("This document can't be saved.")) + } + fn resolve_location(&mut self, loc: Location) -> Option { if self.pages_count() == 0 { return None; @@ -178,6 +192,11 @@ pub fn open>(path: P) -> Option> { .map_err(|e| eprintln!("{}: {}.", path.as_ref().display(), e)) .map(|d| Box::new(d) as Box).ok() }, + "html" | "htm" => { + HtmlDocument::new(&path) + .map_err(|e| eprintln!("{}: {}.", path.as_ref().display(), e)) + .map(|d| Box::new(d) as Box).ok() + }, "djvu" | "djv" => { DjvuOpener::new().and_then(|o| { o.open(path) @@ -218,23 +237,26 @@ impl From for Location { } pub fn toc_as_html(toc: &[TocEntry], chap_index: usize) -> String { - let mut buf = r#" - - Table of Contents - - - "#.to_string(); - toc_as_html_aux(toc, chap_index, &mut buf); - buf.push_str(""); + let mut buf = "\n\t\n\t\tTable of Contents\n\t\t\ + \n\t\ + \n\t\n".to_string(); + toc_as_html_aux(toc, chap_index, 0, &mut buf); + buf.push_str("\t\n"); buf } -pub fn toc_as_html_aux(toc: &[TocEntry], chap_index: usize, buf: &mut String) { - buf.push_str("
    "); +pub fn toc_as_html_aux(toc: &[TocEntry], chap_index: usize, depth: usize, buf: &mut String) { + buf.push_str(&"\t".repeat(depth + 2)); + if depth == 0 { + buf.push_str("\n"); } #[inline] @@ -361,6 +384,116 @@ pub fn chapter_from_uri<'a>(target_uri: &str, toc: &'a [TocEntry]) -> Option<&'a None } +const HWINFO_KEYS: [&str; 19] = ["CPU", "PCB", "DisplayPanel", "DisplayCtrl", "DisplayBusWidth", + "DisplayResolution", "FrontLight", "FrontLight_LEDrv", "FL_PWM", + "TouchCtrl", "TouchType", "Battery", "IFlash", "RamSize", "RamType", + "LightSensor", "HallSensor", "RSensor", "Wifi"]; + +pub fn sys_info_as_html() -> String { + let mut buf = "\n\t\n\t\tSystem Info\n\t\t\ + \n\t\n\t\n".to_string(); + + buf.push_str("\t\t\n"); + + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", CURRENT_DEVICE.model)); + buf.push_str("\t\t\t\n"); + + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", CURRENT_DEVICE.mark())); + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\n"); + + for (name, var) in [("Code name", "PRODUCT"), + ("Model number", "MODEL_NUMBER"), + ("Firmare version", "FIRMWARE_VERSION")].iter() { + if let Ok(value) = env::var(var) { + buf.push_str("\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", name)); + buf.push_str(&format!("\t\t\t\t\n", value)); + buf.push_str("\t\t\t\n"); + } + } + + buf.push_str("\t\t\t\n"); + + let output = Command::new("scripts/ip.sh") + .output() + .map_err(|e| eprintln!("Can't execute command: {}", e)) + .ok(); + + if let Some(stdout) = output.filter(|output| output.status.success()) + .and_then(|output| String::from_utf8(output.stdout).ok()) + .filter(|stdout| !stdout.is_empty()) { + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", stdout)); + buf.push_str("\t\t\t\n"); + } + + if let Ok(info) = statvfs::statvfs(INTERNAL_CARD_ROOT) { + let fbs = info.fragment_size() as u64; + let free = info.blocks_free() as u64 * fbs; + let total = info.blocks() as u64 * fbs; + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", free.human_size(), total.human_size())); + buf.push_str("\t\t\t\n"); + } + + #[cfg(target_os = "linux")] + if let Ok(info) = sysinfo::sysinfo() { + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", + info.ram_unused().human_size(), + info.ram_total().human_size())); + buf.push_str("\t\t\t\n"); + let load = info.load_average(); + buf.push_str("\t\t\t\n"); + buf.push_str("\t\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", + load.0 * 100.0, + load.1 * 100.0, + load.2 * 100.0)); + buf.push_str("\t\t\t\n"); + } + + buf.push_str("\t\t\t\n"); + + let output = Command::new("/bin/ntx_hwconfig") + .args(&["-s", "/dev/mmcblk0"]) + .output() + .map_err(|e| eprintln!("Can't execute command: {}", e)) + .ok(); + + let mut map = FxHashMap::default(); + + if let Some(stdout) = output.and_then(|output| String::from_utf8(output.stdout).ok()) { + let re = Regex::new(r#"\[\d+\]\s+(?P[^=]+)='(?P[^']+)'"#).unwrap(); + for caps in re.captures_iter(&stdout) { + map.insert(caps["key"].to_string(), caps["value"].to_string()); + } + } + + if !map.is_empty() { + for key in HWINFO_KEYS.iter() { + if let Some(value) = map.get(*key) { + buf.push_str("\t\t\t\n"); + buf.push_str(&format!("\t\t\t\t\n", key)); + buf.push_str(&format!("\t\t\t\t\n", value)); + buf.push_str("\t\t\t\n"); + } + } + } + + buf.push_str("\t\t
    Model name{}
    HardwareMark {}
    {}{}
    IP Address{}
    Storage (Free / Total){} / {}
    Memory (Free / Total){} / {}
    Load Average{:.1}% {:.1}% {:.1}%
    {}{}
    \n\t\n"); + buf +} + // cd mupdf/source && awk '/_extensions\[/,/}/' */*.c lazy_static! { pub static ref RECOGNIZED_KINDS: FxHashSet<&'static str> = diff --git a/src/emulator.rs b/src/emulator.rs index cae1806..8a7cca8 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -38,6 +38,7 @@ use sdl2::rect::Point as SdlPoint; use sdl2::rect::Rect as SdlRect; use crate::framebuffer::{Framebuffer, UpdateMode}; use crate::input::{DeviceEvent, FingerStatus}; +use crate::document::sys_info_as_html; use crate::view::{View, Event, ViewId, EntryId, AppCmd, EntryKind}; use crate::view::{process_render_queue, handle_event, RenderQueue, RenderData}; use crate::view::home::Home; @@ -424,6 +425,15 @@ fn main() -> Result<(), Error> { rq.add(RenderData::new(dialog.id(), *dialog.rect(), UpdateMode::Gui)); view.children_mut().push(Box::new(dialog) as Box); }, + Event::Select(EntryId::SystemInfo) => { + view.children_mut().retain(|child| !child.is::()); + let html = sys_info_as_html(); + let r = Reader::from_html(context.fb.rect(), &html, &tx, &mut context); + let mut next_view = Box::new(r) as Box; + transfer_notifications(view.as_mut(), next_view.as_mut(), &mut rq, &mut context); + history.push(view as Box); + view = next_view; + }, Event::Select(EntryId::Rotate(n)) if n != context.display.rotation && view.might_rotate() => { updating.retain(|tok, _| context.fb.wait(*tok).is_err()); if let Ok(dims) = context.fb.set_rotation(n) { diff --git a/src/view/common.rs b/src/view/common.rs index 3569ac4..b74818b 100644 --- a/src/view/common.rs +++ b/src/view/common.rs @@ -89,6 +89,8 @@ pub fn toggle_main_menu(view: &mut dyn View, rect: Rectangle, enable: Option Reader { + let id = ID_FEEDER.next(); + + let mut info = Info { + file: FileInfo { + path: PathBuf::from(MEM_SCHEME), + kind: "html".to_string(), + size: html.len() as u64, + }, + .. Default::default() + }; + + let mut doc = HtmlDocument::new_from_memory(html); + let (width, height) = context.display.dims; + let font_size = context.settings.reader.font_size; + doc.layout(width, height, font_size, CURRENT_DEVICE.dpi); + let pages_count = doc.pages_count(); + info.title = doc.title().unwrap_or_default(); + + hub.send(Event::Update(UpdateMode::Partial)).ok(); + + Reader { + id, + rect, + children: vec![], + doc: Arc::new(Mutex::new(Box::new(doc))), + cache: BTreeMap::new(), + text: FxHashMap::default(), + annotations: FxHashMap::default(), + chunks: Vec::new(), + focus: None, + search: None, + search_direction: LinearDir::Forward, + held_buttons: FxHashSet::default(), + selection: None, + target_annotation: None, + history: VecDeque::new(), + state: State::Idle, + info, + current_page: 0, + pages_count, + view_port: ViewPort::default(), + synthetic: false, + page_turns: 0, + contrast: Contrast::default(), + ephemeral: true, + reflowable: true, + finished: false, + } + } + fn load_pixmap(&mut self, location: usize) { if self.cache.contains_key(&location) { return; @@ -1635,25 +1686,31 @@ impl Reader { return; } - if self.reflowable { - return; - } + let entries = if self.reflowable { + if self.ephemeral { + vec![EntryKind::Command("Save".to_string(), EntryId::Save)] + } else { + Vec::new() + } + } else { + let zoom_mode = self.view_port.zoom_mode; + vec![EntryKind::SubMenu("Zoom Mode".to_string(), vec![ + EntryKind::RadioButton("Fit to Page".to_string(), + EntryId::SetZoomMode(ZoomMode::FitToPage), + zoom_mode == ZoomMode::FitToPage), + EntryKind::RadioButton("Fit to Width".to_string(), + EntryId::SetZoomMode(ZoomMode::FitToWidth), + zoom_mode == ZoomMode::FitToWidth)])] + }; - let zoom_mode = self.view_port.zoom_mode; - let entries = vec![EntryKind::SubMenu("Zoom Mode".to_string(), vec![ - EntryKind::RadioButton("Fit to Page".to_string(), - EntryId::SetZoomMode(ZoomMode::FitToPage), - zoom_mode == ZoomMode::FitToPage), - EntryKind::RadioButton("Fit to Width".to_string(), - EntryId::SetZoomMode(ZoomMode::FitToWidth), - zoom_mode == ZoomMode::FitToWidth)])]; - let title_menu = Menu::new(rect, ViewId::TitleMenu, MenuKind::DropDown, entries, context); - rq.add(RenderData::new(title_menu.id(), *title_menu.rect(), UpdateMode::Gui)); - self.children.push(Box::new(title_menu) as Box); + if !entries.is_empty() { + let title_menu = Menu::new(rect, ViewId::TitleMenu, MenuKind::DropDown, entries, context); + rq.add(RenderData::new(title_menu.id(), *title_menu.rect(), UpdateMode::Gui)); + self.children.push(Box::new(title_menu) as Box); + } } } - fn toggle_font_family_menu(&mut self, rect: Rectangle, enable: Option, rq: &mut RenderQueue, context: &mut Context) { if let Some(index) = locate_by_id(self, ViewId::FontFamilyMenu) { if let Some(true) = enable { @@ -2812,7 +2869,7 @@ impl View for Reader { // Bottom left corner. } else if dc > 0 && center.y > self.rect.max.y - dc { if self.search.is_none() { - if self.ephemeral { + if self.ephemeral && self.info.file.path == PathBuf::from(TOC_SCHEME) { self.quit(context); hub.send(Event::Back).ok(); } else { @@ -3352,6 +3409,19 @@ impl View for Reader { self.set_zoom_mode(zoom_mode, hub, rq, context); true }, + Event::Select(EntryId::Save) => { + let name = format!("{}-{}.{}", self.info.title.to_lowercase().replace(' ', "_"), + Local::now().format("%Y%m%d_%H%M%S"), + self.info.file.kind); + let doc = self.doc.lock().unwrap(); + let msg = match doc.save(&name) { + Err(e) => format!("{}", e), + Ok(()) => format!("Saved {}.", name), + }; + let notif = Notification::new(ViewId::SaveDocumentNotif, msg, hub, rq, context); + self.children.push(Box::new(notif) as Box); + true + }, Event::Select(EntryId::ApplyCroppings(index, scheme)) => { self.info.reader.as_mut().map(|r| { if r.cropping_margins.is_none() {