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.

546 lines
21KB

  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. account_pos: usize,
  51. cmd_buf: String,
  52. id: ComponentId,
  53. }
  54. impl fmt::Display for EnvelopeView {
  55. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  56. // TODO display subject/info
  57. write!(f, "view mail")
  58. }
  59. }
  60. impl EnvelopeView {
  61. pub fn new(
  62. wrapper: EnvelopeWrapper,
  63. pager: Option<Pager>,
  64. subview: Option<Box<Component>>,
  65. account_pos: usize,
  66. ) -> Self {
  67. EnvelopeView {
  68. pager,
  69. subview,
  70. dirty: true,
  71. mode: ViewMode::Normal,
  72. wrapper,
  73. account_pos,
  74. cmd_buf: String::with_capacity(4),
  75. id: ComponentId::new_v4(),
  76. }
  77. }
  78. /// Returns the string to be displayed in the Viewer
  79. fn attachment_to_text<'closure, 's: 'closure, 'context: 's>(
  80. &'s self,
  81. body: &'context Attachment,
  82. context: &'context mut Context,
  83. ) -> String {
  84. let finder = LinkFinder::new();
  85. let body_text = String::from_utf8_lossy(&decode_rec(
  86. &body,
  87. Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
  88. if a.content_type().is_text_html() {
  89. use std::io::Write;
  90. use std::process::{Command, Stdio};
  91. let settings = context.accounts[self.account_pos].runtime_settings.conf();
  92. if let Some(filter_invocation) = settings.html_filter() {
  93. let parts = split_command!(filter_invocation);
  94. let (cmd, args) = (parts[0], &parts[1..]);
  95. let command_obj = Command::new(cmd)
  96. .args(args)
  97. .stdin(Stdio::piped())
  98. .stdout(Stdio::piped())
  99. .spawn();
  100. if command_obj.is_err() {
  101. context.replies.push_back(UIEvent::Notification(
  102. Some(format!(
  103. "Failed to start html filter process: {}",
  104. filter_invocation,
  105. )),
  106. String::new(),
  107. ));
  108. return;
  109. }
  110. let mut html_filter = command_obj.unwrap();
  111. html_filter
  112. .stdin
  113. .as_mut()
  114. .unwrap()
  115. .write_all(&v)
  116. .expect("Failed to write to stdin");
  117. *v = format!(
  118. "Text piped through `{}`. Press `v` to open in web browser. \n\n",
  119. filter_invocation
  120. )
  121. .into_bytes();
  122. v.extend(html_filter.wait_with_output().unwrap().stdout);
  123. }
  124. }
  125. })),
  126. ))
  127. .into_owned();
  128. match self.mode {
  129. ViewMode::Normal | ViewMode::Subview => {
  130. let mut t = body_text.to_string();
  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::Raw => String::from_utf8_lossy(body.bytes()).into_owned(),
  144. ViewMode::Url => {
  145. let mut t = body_text.to_string();
  146. for (lidx, l) in finder.links(&body.text()).enumerate() {
  147. let offset = if lidx < 10 {
  148. lidx * 3
  149. } else if lidx < 100 {
  150. 26 + (lidx - 9) * 4
  151. } else if lidx < 1000 {
  152. 385 + (lidx - 99) * 5
  153. } else {
  154. panic!("BUG: Message body with more than 100 urls, fix this");
  155. };
  156. t.insert_str(l.start() + offset, &format!("[{}]", lidx));
  157. }
  158. if body.count_attachments() > 1 {
  159. t = body
  160. .attachments()
  161. .iter()
  162. .enumerate()
  163. .fold(t, |mut s, (idx, a)| {
  164. s.push_str(&format!("[{}] {}\n\n", idx, a));
  165. s
  166. });
  167. }
  168. t
  169. }
  170. ViewMode::Attachment(aidx) => {
  171. let attachments = body.attachments();
  172. let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
  173. ret.push_str(&attachments[aidx].text());
  174. ret
  175. }
  176. }
  177. }
  178. /*
  179. * TODO: add recolor changes so that this function returns a vector of required highlights
  180. * to pass to write_string...
  181. pub fn plain_text_to_buf(s: &str, highlight_urls: bool) -> CellBuffer {
  182. let mut buf = CellBuffer::from(s);
  183. if highlight_urls {
  184. let lines: Vec<&str> = s.split('\n').map(|l| l.trim_right()).collect();
  185. let mut shift = 0;
  186. let mut lidx_total = 0;
  187. let finder = LinkFinder::new();
  188. for r in &lines {
  189. for l in finder.links(&r) {
  190. let offset = if lidx_total < 10 {
  191. 3
  192. } else if lidx_total < 100 {
  193. 4
  194. } else if lidx_total < 1000 {
  195. 5
  196. } else {
  197. panic!("BUG: Message body with more than 100 urls");
  198. };
  199. for i in 1..=offset {
  200. buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226));
  201. //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226));
  202. //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226));
  203. }
  204. lidx_total += 1;
  205. }
  206. // Each Cell represents one char so next line will be:
  207. shift += r.chars().count() + 1;
  208. }
  209. }
  210. buf
  211. }
  212. */
  213. }
  214. impl Component for EnvelopeView {
  215. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  216. let upper_left = upper_left!(area);
  217. let bottom_right = bottom_right!(area);
  218. let y: usize = {
  219. let envelope: &Envelope = &self.wrapper;
  220. if self.mode == ViewMode::Raw {
  221. clear_area(grid, area);
  222. context.dirty_areas.push_back(area);
  223. get_y(upper_left) - 1
  224. } else {
  225. let (x, y) = write_string_to_grid(
  226. &format!("Date: {}", envelope.date_as_str()),
  227. grid,
  228. Color::Byte(33),
  229. Color::Default,
  230. area,
  231. true,
  232. );
  233. for x in x..=get_x(bottom_right) {
  234. grid[(x, y)].set_ch(' ');
  235. grid[(x, y)].set_bg(Color::Default);
  236. grid[(x, y)].set_fg(Color::Default);
  237. }
  238. let (x, y) = write_string_to_grid(
  239. &format!("From: {}", envelope.field_from_to_string()),
  240. grid,
  241. Color::Byte(33),
  242. Color::Default,
  243. (set_y(upper_left, y + 1), bottom_right),
  244. true,
  245. );
  246. for x in x..=get_x(bottom_right) {
  247. grid[(x, y)].set_ch(' ');
  248. grid[(x, y)].set_bg(Color::Default);
  249. grid[(x, y)].set_fg(Color::Default);
  250. }
  251. let (x, y) = write_string_to_grid(
  252. &format!("To: {}", envelope.field_to_to_string()),
  253. grid,
  254. Color::Byte(33),
  255. Color::Default,
  256. (set_y(upper_left, y + 1), bottom_right),
  257. true,
  258. );
  259. for x in x..=get_x(bottom_right) {
  260. grid[(x, y)].set_ch(' ');
  261. grid[(x, y)].set_bg(Color::Default);
  262. grid[(x, y)].set_fg(Color::Default);
  263. }
  264. let (x, y) = write_string_to_grid(
  265. &format!("Subject: {}", envelope.subject()),
  266. grid,
  267. Color::Byte(33),
  268. Color::Default,
  269. (set_y(upper_left, y + 1), bottom_right),
  270. true,
  271. );
  272. for x in x..=get_x(bottom_right) {
  273. grid[(x, y)].set_ch(' ');
  274. grid[(x, y)].set_bg(Color::Default);
  275. grid[(x, y)].set_fg(Color::Default);
  276. }
  277. let (x, y) = write_string_to_grid(
  278. &format!("Message-ID: <{}>", envelope.message_id_raw()),
  279. grid,
  280. Color::Byte(33),
  281. Color::Default,
  282. (set_y(upper_left, y + 1), bottom_right),
  283. true,
  284. );
  285. for x in x..=get_x(bottom_right) {
  286. grid[(x, y)].set_ch(' ');
  287. grid[(x, y)].set_bg(Color::Default);
  288. grid[(x, y)].set_fg(Color::Default);
  289. }
  290. clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)));
  291. context
  292. .dirty_areas
  293. .push_back((upper_left, set_y(bottom_right, y + 1)));
  294. y + 1
  295. }
  296. };
  297. if self.dirty {
  298. let body = self.wrapper.body_bytes(self.wrapper.buffer());
  299. match self.mode {
  300. ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
  301. let attachment = &body.attachments()[aidx];
  302. self.subview = Some(Box::new(HtmlView::new(
  303. &attachment,
  304. context,
  305. self.account_pos,
  306. )));
  307. }
  308. ViewMode::Normal if body.is_html() => {
  309. self.subview = Some(Box::new(HtmlView::new(&body, context, self.account_pos)));
  310. self.mode = ViewMode::Subview;
  311. }
  312. _ => {
  313. let text = {
  314. self.attachment_to_text(&body, context)
  315. /*
  316. let text = self.attachment_to_text(&body);
  317. // URL indexes must be colored (ugh..)
  318. EnvelopeView::plain_text_to_buf(&text, self.mode == ViewMode::Url)
  319. */
  320. };
  321. let cursor_pos = if self.mode.is_attachment() {
  322. Some(0)
  323. } else {
  324. self.pager.as_ref().map(Pager::cursor_pos)
  325. };
  326. self.pager = Some(Pager::from_string(
  327. text,
  328. Some(context),
  329. cursor_pos,
  330. Some(width!(area)),
  331. ));
  332. }
  333. };
  334. self.dirty = false;
  335. }
  336. if let Some(s) = self.subview.as_mut() {
  337. s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  338. } else if let Some(p) = self.pager.as_mut() {
  339. p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  340. }
  341. }
  342. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  343. if let Some(ref mut sub) = self.subview {
  344. if sub.process_event(event, context) {
  345. return true;
  346. }
  347. } else if let Some(ref mut p) = self.pager {
  348. if p.process_event(event, context) {
  349. return true;
  350. }
  351. }
  352. match *event {
  353. UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) => {
  354. self.cmd_buf.clear();
  355. context
  356. .replies
  357. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  358. return true;
  359. }
  360. UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
  361. self.cmd_buf.push(c);
  362. return true;
  363. }
  364. UIEvent::Input(Key::Char('r'))
  365. if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw =>
  366. {
  367. self.mode = if self.mode == ViewMode::Raw {
  368. ViewMode::Normal
  369. } else {
  370. ViewMode::Raw
  371. };
  372. self.dirty = true;
  373. return true;
  374. }
  375. UIEvent::Input(Key::Char('r'))
  376. if self.mode.is_attachment() || self.mode == ViewMode::Subview =>
  377. {
  378. self.mode = ViewMode::Normal;
  379. self.subview.take();
  380. self.dirty = true;
  381. return true;
  382. }
  383. UIEvent::Input(Key::Char('a'))
  384. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal =>
  385. {
  386. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  387. self.cmd_buf.clear();
  388. context
  389. .replies
  390. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  391. {
  392. let envelope: &Envelope = self.wrapper.envelope();
  393. if let Some(u) = envelope
  394. .body_bytes(self.wrapper.buffer())
  395. .attachments()
  396. .get(lidx)
  397. {
  398. match u.content_type() {
  399. ContentType::MessageRfc822 => {
  400. self.mode = ViewMode::Subview;
  401. self.subview = Some(Box::new(Pager::from_string(
  402. String::from_utf8_lossy(&decode_rec(u, None)).to_string(),
  403. Some(context),
  404. None,
  405. None,
  406. )));
  407. }
  408. ContentType::Text { .. } => {
  409. self.mode = ViewMode::Attachment(lidx);
  410. self.dirty = true;
  411. }
  412. ContentType::Multipart { .. } => {
  413. context.replies.push_back(UIEvent::StatusEvent(
  414. StatusEvent::DisplayMessage(
  415. "Multipart attachments are not supported yet.".to_string(),
  416. ),
  417. ));
  418. return true;
  419. }
  420. ContentType::Unsupported { .. } => {
  421. let attachment_type = u.mime_type();
  422. let binary = query_default_app(&attachment_type);
  423. if let Ok(binary) = binary {
  424. let p = create_temp_file(&decode(u, None), None);
  425. Command::new(&binary)
  426. .arg(p.path())
  427. .stdin(Stdio::piped())
  428. .stdout(Stdio::piped())
  429. .spawn()
  430. .unwrap_or_else(|_| {
  431. panic!("Failed to start {}", binary.display())
  432. });
  433. context.temp_files.push(p);
  434. } else {
  435. context.replies.push_back(UIEvent::StatusEvent(
  436. StatusEvent::DisplayMessage(format!(
  437. "Couldn't find a default application for type {}",
  438. attachment_type
  439. )),
  440. ));
  441. return true;
  442. }
  443. }
  444. ContentType::PGPSignature => {
  445. context.replies.push_back(UIEvent::StatusEvent(
  446. StatusEvent::DisplayMessage(
  447. "Signatures aren't supported yet".to_string(),
  448. ),
  449. ));
  450. return true;
  451. }
  452. }
  453. } else {
  454. context.replies.push_back(UIEvent::StatusEvent(
  455. StatusEvent::DisplayMessage(format!(
  456. "Attachment `{}` not found.",
  457. lidx
  458. )),
  459. ));
  460. return true;
  461. }
  462. };
  463. return true;
  464. }
  465. UIEvent::Input(Key::Char('g'))
  466. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
  467. {
  468. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  469. self.cmd_buf.clear();
  470. context
  471. .replies
  472. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  473. let url = {
  474. let envelope: &Envelope = self.wrapper.envelope();
  475. let finder = LinkFinder::new();
  476. let t = envelope
  477. .body_bytes(self.wrapper.buffer())
  478. .text()
  479. .to_string();
  480. let links: Vec<Link> = finder.links(&t).collect();
  481. if let Some(u) = links.get(lidx) {
  482. u.as_str().to_string()
  483. } else {
  484. context.replies.push_back(UIEvent::StatusEvent(
  485. StatusEvent::DisplayMessage(format!("Link `{}` not found.", lidx)),
  486. ));
  487. return true;
  488. }
  489. };
  490. Command::new("xdg-open")
  491. .arg(url)
  492. .stdin(Stdio::piped())
  493. .stdout(Stdio::piped())
  494. .spawn()
  495. .expect("Failed to start xdg_open");
  496. return true;
  497. }
  498. UIEvent::Input(Key::Char('u')) => {
  499. match self.mode {
  500. ViewMode::Normal => self.mode = ViewMode::Url,
  501. ViewMode::Url => self.mode = ViewMode::Normal,
  502. _ => {}
  503. }
  504. self.dirty = true;
  505. return true;
  506. }
  507. _ => {}
  508. }
  509. false
  510. }
  511. fn is_dirty(&self) -> bool {
  512. self.dirty
  513. || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  514. || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  515. }
  516. fn set_dirty(&mut self) {
  517. self.dirty = true;
  518. }
  519. fn id(&self) -> ComponentId {
  520. self.id
  521. }
  522. fn set_id(&mut self, id: ComponentId) {
  523. self.id = id;
  524. }
  525. }