You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

505 lines
19KB

  1. /*
  2. * meli - ui crate.
  3. *
  4. * Copyright 2017-2018 Manos Pitsidianakis
  5. *
  6. * This file is part of meli.
  7. *
  8. * meli is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * meli is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with meli. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. use super::*;
  22. use linkify::{Link, LinkFinder};
  23. use std::process::{Command, Stdio};
  24. use mime_apps::query_default_app;
  25. #[derive(PartialEq, Debug)]
  26. enum ViewMode {
  27. Normal,
  28. Url,
  29. Attachment(usize),
  30. Raw,
  31. Subview,
  32. }
  33. impl ViewMode {
  34. fn is_attachment(&self) -> bool {
  35. match self {
  36. ViewMode::Attachment(_) => true,
  37. _ => false,
  38. }
  39. }
  40. }
  41. /// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
  42. /// menus
  43. #[derive(Debug)]
  44. pub struct EnvelopeView {
  45. pager: Option<Pager>,
  46. subview: Option<Box<Component>>,
  47. dirty: bool,
  48. mode: ViewMode,
  49. wrapper: EnvelopeWrapper,
  50. cmd_buf: String,
  51. }
  52. impl fmt::Display for EnvelopeView {
  53. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  54. // TODO display subject/info
  55. write!(f, "view mail")
  56. }
  57. }
  58. impl EnvelopeView {
  59. pub fn new(
  60. wrapper: EnvelopeWrapper,
  61. pager: Option<Pager>,
  62. subview: Option<Box<Component>>,
  63. ) -> Self {
  64. EnvelopeView {
  65. pager,
  66. subview,
  67. dirty: true,
  68. mode: ViewMode::Normal,
  69. wrapper,
  70. cmd_buf: String::with_capacity(4),
  71. }
  72. }
  73. /// Returns the string to be displayed in the Viewer
  74. fn attachment_to_text(&self, body: &Attachment) -> String {
  75. let finder = LinkFinder::new();
  76. let body_text = String::from_utf8_lossy(&decode_rec(
  77. &body,
  78. Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
  79. if a.content_type().is_text_html() {
  80. use std::io::Write;
  81. use std::process::{Command, Stdio};
  82. let mut html_filter = Command::new("w3m")
  83. .args(&["-I", "utf-8", "-T", "text/html"])
  84. .stdin(Stdio::piped())
  85. .stdout(Stdio::piped())
  86. .spawn()
  87. .expect("Failed to start html filter process");
  88. html_filter
  89. .stdin
  90. .as_mut()
  91. .unwrap()
  92. .write_all(&v)
  93. .expect("Failed to write to w3m stdin");
  94. *v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n"
  95. .to_vec();
  96. v.extend(html_filter.wait_with_output().unwrap().stdout);
  97. }
  98. })),
  99. ))
  100. .into_owned();
  101. match self.mode {
  102. ViewMode::Normal | ViewMode::Subview => {
  103. let mut t = body_text.to_string();
  104. if body.count_attachments() > 1 {
  105. t = body
  106. .attachments()
  107. .iter()
  108. .enumerate()
  109. .fold(t, |mut s, (idx, a)| {
  110. s.push_str(&format!("[{}] {}\n\n", idx, a));
  111. s
  112. });
  113. }
  114. t
  115. }
  116. ViewMode::Raw => String::from_utf8_lossy(body.bytes()).into_owned(),
  117. ViewMode::Url => {
  118. let mut t = body_text.to_string();
  119. for (lidx, l) in finder.links(&body.text()).enumerate() {
  120. let offset = if lidx < 10 {
  121. lidx * 3
  122. } else if lidx < 100 {
  123. 26 + (lidx - 9) * 4
  124. } else if lidx < 1000 {
  125. 385 + (lidx - 99) * 5
  126. } else {
  127. panic!("BUG: Message body with more than 100 urls, fix this");
  128. };
  129. t.insert_str(l.start() + offset, &format!("[{}]", lidx));
  130. }
  131. if body.count_attachments() > 1 {
  132. t = body
  133. .attachments()
  134. .iter()
  135. .enumerate()
  136. .fold(t, |mut s, (idx, a)| {
  137. s.push_str(&format!("[{}] {}\n\n", idx, a));
  138. s
  139. });
  140. }
  141. t
  142. }
  143. ViewMode::Attachment(aidx) => {
  144. let attachments = body.attachments();
  145. let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
  146. ret.push_str(&attachments[aidx].text());
  147. ret
  148. }
  149. }
  150. }
  151. pub fn plain_text_to_buf(s: &str, highlight_urls: bool) -> CellBuffer {
  152. let mut buf = CellBuffer::from(s);
  153. if highlight_urls {
  154. let lines: Vec<&str> = s.split('\n').map(|l| l.trim_right()).collect();
  155. let mut shift = 0;
  156. let mut lidx_total = 0;
  157. let finder = LinkFinder::new();
  158. for r in &lines {
  159. for l in finder.links(&r) {
  160. let offset = if lidx_total < 10 {
  161. 3
  162. } else if lidx_total < 100 {
  163. 4
  164. } else if lidx_total < 1000 {
  165. 5
  166. } else {
  167. panic!("BUG: Message body with more than 100 urls");
  168. };
  169. for i in 1..=offset {
  170. buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226));
  171. //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226));
  172. //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226));
  173. }
  174. lidx_total += 1;
  175. }
  176. // Each Cell represents one char so next line will be:
  177. shift += r.chars().count() + 1;
  178. }
  179. }
  180. buf
  181. }
  182. }
  183. impl Component for EnvelopeView {
  184. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  185. let upper_left = upper_left!(area);
  186. let bottom_right = bottom_right!(area);
  187. let y: usize = {
  188. let envelope: &Envelope = &self.wrapper;
  189. if self.mode == ViewMode::Raw {
  190. clear_area(grid, area);
  191. context.dirty_areas.push_back(area);
  192. get_y(upper_left) - 1
  193. } else {
  194. let (x, y) = write_string_to_grid(
  195. &format!("Date: {}", envelope.date_as_str()),
  196. grid,
  197. Color::Byte(33),
  198. Color::Default,
  199. area,
  200. true,
  201. );
  202. for x in x..=get_x(bottom_right) {
  203. grid[(x, y)].set_ch(' ');
  204. grid[(x, y)].set_bg(Color::Default);
  205. grid[(x, y)].set_fg(Color::Default);
  206. }
  207. let (x, y) = write_string_to_grid(
  208. &format!("From: {}", envelope.field_from_to_string()),
  209. grid,
  210. Color::Byte(33),
  211. Color::Default,
  212. (set_y(upper_left, y + 1), bottom_right),
  213. true,
  214. );
  215. for x in x..=get_x(bottom_right) {
  216. grid[(x, y)].set_ch(' ');
  217. grid[(x, y)].set_bg(Color::Default);
  218. grid[(x, y)].set_fg(Color::Default);
  219. }
  220. let (x, y) = write_string_to_grid(
  221. &format!("To: {}", envelope.field_to_to_string()),
  222. grid,
  223. Color::Byte(33),
  224. Color::Default,
  225. (set_y(upper_left, y + 1), bottom_right),
  226. true,
  227. );
  228. for x in x..=get_x(bottom_right) {
  229. grid[(x, y)].set_ch(' ');
  230. grid[(x, y)].set_bg(Color::Default);
  231. grid[(x, y)].set_fg(Color::Default);
  232. }
  233. let (x, y) = write_string_to_grid(
  234. &format!("Subject: {}", envelope.subject()),
  235. grid,
  236. Color::Byte(33),
  237. Color::Default,
  238. (set_y(upper_left, y + 1), bottom_right),
  239. true,
  240. );
  241. for x in x..=get_x(bottom_right) {
  242. grid[(x, y)].set_ch(' ');
  243. grid[(x, y)].set_bg(Color::Default);
  244. grid[(x, y)].set_fg(Color::Default);
  245. }
  246. let (x, y) = write_string_to_grid(
  247. &format!("Message-ID: <{}>", envelope.message_id_raw()),
  248. grid,
  249. Color::Byte(33),
  250. Color::Default,
  251. (set_y(upper_left, y + 1), bottom_right),
  252. true,
  253. );
  254. for x in x..=get_x(bottom_right) {
  255. grid[(x, y)].set_ch(' ');
  256. grid[(x, y)].set_bg(Color::Default);
  257. grid[(x, y)].set_fg(Color::Default);
  258. }
  259. clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)));
  260. context
  261. .dirty_areas
  262. .push_back((upper_left, set_y(bottom_right, y + 1)));
  263. y + 1
  264. }
  265. };
  266. if self.dirty {
  267. let body = self.wrapper.body_bytes(self.wrapper.buffer());
  268. match self.mode {
  269. ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
  270. self.subview = Some(Box::new(HtmlView::new(decode(
  271. &body.attachments()[aidx],
  272. None,
  273. ))));
  274. }
  275. ViewMode::Normal if body.is_html() => {
  276. self.subview = Some(Box::new(HtmlView::new(decode(&body, None))));
  277. self.mode = ViewMode::Subview;
  278. }
  279. _ => {
  280. let buf = {
  281. let text = self.attachment_to_text(&body);
  282. // URL indexes must be colored (ugh..)
  283. EnvelopeView::plain_text_to_buf(&text, self.mode == ViewMode::Url)
  284. };
  285. let cursor_pos = if self.mode.is_attachment() {
  286. Some(0)
  287. } else {
  288. self.pager.as_mut().map(|p| p.cursor_pos())
  289. };
  290. self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos));
  291. }
  292. };
  293. self.dirty = false;
  294. }
  295. if let Some(s) = self.subview.as_mut() {
  296. s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  297. } else if let Some(p) = self.pager.as_mut() {
  298. p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  299. }
  300. }
  301. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  302. if let Some(ref mut sub) = self.subview {
  303. if sub.process_event(event, context) {
  304. return true;
  305. }
  306. } else if let Some(ref mut p) = self.pager {
  307. if p.process_event(event, context) {
  308. return true;
  309. }
  310. }
  311. match event.event_type {
  312. UIEventType::Input(Key::Esc) | UIEventType::Input(Key::Alt('')) => {
  313. self.cmd_buf.clear();
  314. context.replies.push_back(UIEvent {
  315. id: 0,
  316. event_type: UIEventType::StatusEvent(StatusEvent::BufClear),
  317. });
  318. return true;
  319. }
  320. UIEventType::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
  321. self.cmd_buf.push(c);
  322. return true;
  323. }
  324. UIEventType::Input(Key::Char('r'))
  325. if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw =>
  326. {
  327. self.mode = if self.mode == ViewMode::Raw {
  328. ViewMode::Normal
  329. } else {
  330. ViewMode::Raw
  331. };
  332. self.dirty = true;
  333. return true;
  334. }
  335. UIEventType::Input(Key::Char('r'))
  336. if self.mode.is_attachment() || self.mode == ViewMode::Subview =>
  337. {
  338. self.mode = ViewMode::Normal;
  339. self.subview.take();
  340. self.dirty = true;
  341. return true;
  342. }
  343. UIEventType::Input(Key::Char('a'))
  344. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal =>
  345. {
  346. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  347. self.cmd_buf.clear();
  348. context.replies.push_back(UIEvent {
  349. id: 0,
  350. event_type: UIEventType::StatusEvent(StatusEvent::BufClear),
  351. });
  352. {
  353. let envelope: &Envelope = self.wrapper.envelope();
  354. if let Some(u) = envelope
  355. .body_bytes(self.wrapper.buffer())
  356. .attachments()
  357. .get(lidx)
  358. {
  359. match u.content_type() {
  360. ContentType::MessageRfc822 => {
  361. self.mode = ViewMode::Subview;
  362. self.subview = Some(Box::new(Pager::from_string(
  363. String::from_utf8_lossy(&decode_rec(u, None)).to_string(),
  364. context,
  365. None,
  366. None,
  367. )));
  368. }
  369. ContentType::Text { .. } => {
  370. self.mode = ViewMode::Attachment(lidx);
  371. self.dirty = true;
  372. }
  373. ContentType::Multipart { .. } => {
  374. context.replies.push_back(UIEvent {
  375. id: 0,
  376. event_type: UIEventType::StatusEvent(
  377. StatusEvent::DisplayMessage(
  378. "Multipart attachments are not supported yet."
  379. .to_string(),
  380. ),
  381. ),
  382. });
  383. return true;
  384. }
  385. ContentType::Unsupported { .. } => {
  386. let attachment_type = u.mime_type();
  387. let binary = query_default_app(&attachment_type);
  388. if let Ok(binary) = binary {
  389. let mut p = create_temp_file(&decode(u, None), None);
  390. Command::new(&binary)
  391. .arg(p.path())
  392. .stdin(Stdio::piped())
  393. .stdout(Stdio::piped())
  394. .spawn()
  395. .unwrap_or_else(|_| {
  396. panic!("Failed to start {}", binary.display())
  397. });
  398. context.temp_files.push(p);
  399. } else {
  400. context.replies.push_back(UIEvent {
  401. id: 0,
  402. event_type: UIEventType::StatusEvent(
  403. StatusEvent::DisplayMessage(format!(
  404. "Couldn't find a default application for type {}",
  405. attachment_type
  406. )),
  407. ),
  408. });
  409. return true;
  410. }
  411. }
  412. }
  413. } else {
  414. context.replies.push_back(UIEvent {
  415. id: 0,
  416. event_type: UIEventType::StatusEvent(StatusEvent::DisplayMessage(
  417. format!("Attachment `{}` not found.", lidx),
  418. )),
  419. });
  420. return true;
  421. }
  422. };
  423. return true;
  424. }
  425. UIEventType::Input(Key::Char('g'))
  426. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
  427. {
  428. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  429. self.cmd_buf.clear();
  430. context.replies.push_back(UIEvent {
  431. id: 0,
  432. event_type: UIEventType::StatusEvent(StatusEvent::BufClear),
  433. });
  434. let url = {
  435. let envelope: &Envelope = self.wrapper.envelope();
  436. let finder = LinkFinder::new();
  437. let mut t = envelope
  438. .body_bytes(self.wrapper.buffer())
  439. .text()
  440. .to_string();
  441. let links: Vec<Link> = finder.links(&t).collect();
  442. if let Some(u) = links.get(lidx) {
  443. u.as_str().to_string()
  444. } else {
  445. context.replies.push_back(UIEvent {
  446. id: 0,
  447. event_type: UIEventType::StatusEvent(StatusEvent::DisplayMessage(
  448. format!("Link `{}` not found.", lidx),
  449. )),
  450. });
  451. return true;
  452. }
  453. };
  454. Command::new("xdg-open")
  455. .arg(url)
  456. .stdin(Stdio::piped())
  457. .stdout(Stdio::piped())
  458. .spawn()
  459. .expect("Failed to start xdg_open");
  460. return true;
  461. }
  462. UIEventType::Input(Key::Char('u')) => {
  463. match self.mode {
  464. ViewMode::Normal => self.mode = ViewMode::Url,
  465. ViewMode::Url => self.mode = ViewMode::Normal,
  466. _ => {}
  467. }
  468. self.dirty = true;
  469. return true;
  470. }
  471. _ => {}
  472. }
  473. false
  474. }
  475. fn is_dirty(&self) -> bool {
  476. self.dirty
  477. || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  478. || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  479. }
  480. fn set_dirty(&mut self) {
  481. self.dirty = true;
  482. }
  483. }