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.

812 lines
32KB

  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. mod html;
  25. pub use self::html::*;
  26. mod thread;
  27. pub use self::thread::*;
  28. mod envelope;
  29. pub use self::envelope::*;
  30. use mime_apps::query_default_app;
  31. #[derive(PartialEq, Debug)]
  32. enum ViewMode {
  33. Normal,
  34. Url,
  35. Attachment(usize),
  36. Raw,
  37. Subview,
  38. ContactSelector(Selector),
  39. }
  40. impl Default for ViewMode {
  41. fn default() -> Self {
  42. ViewMode::Normal
  43. }
  44. }
  45. impl ViewMode {
  46. fn is_attachment(&self) -> bool {
  47. match self {
  48. ViewMode::Attachment(_) => true,
  49. _ => false,
  50. }
  51. }
  52. }
  53. /// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
  54. /// menus
  55. #[derive(Debug, Default)]
  56. pub struct MailView {
  57. coordinates: (usize, usize, EnvelopeHash),
  58. pager: Option<Pager>,
  59. subview: Option<Box<Component>>,
  60. dirty: bool,
  61. mode: ViewMode,
  62. expand_headers: bool,
  63. cmd_buf: String,
  64. id: ComponentId,
  65. }
  66. impl fmt::Display for MailView {
  67. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  68. // TODO display subject/info
  69. write!(f, "{}", MailView::DESCRIPTION)
  70. }
  71. }
  72. impl MailView {
  73. const DESCRIPTION: &'static str = "mail";
  74. pub fn new(
  75. coordinates: (usize, usize, EnvelopeHash),
  76. pager: Option<Pager>,
  77. subview: Option<Box<Component>>,
  78. ) -> Self {
  79. MailView {
  80. coordinates,
  81. pager,
  82. subview,
  83. dirty: true,
  84. mode: ViewMode::Normal,
  85. expand_headers: false,
  86. cmd_buf: String::with_capacity(4),
  87. id: ComponentId::new_v4(),
  88. }
  89. }
  90. /// Returns the string to be displayed in the Viewer
  91. fn attachment_to_text<'closure, 's: 'closure, 'context: 's>(
  92. &'s self,
  93. body: &'context Attachment,
  94. context: &'context mut Context,
  95. ) -> String {
  96. let finder = LinkFinder::new();
  97. let body_text = String::from_utf8_lossy(&decode_rec(
  98. body,
  99. Some(Box::new(move |a: &'closure Attachment, v: &mut Vec<u8>| {
  100. if a.content_type().is_text_html() {
  101. use std::io::Write;
  102. use std::process::{Command, Stdio};
  103. let settings = context.accounts[self.coordinates.0].runtime_settings.conf();
  104. /* FIXME: duplication with view/html.rs */
  105. if let Some(filter_invocation) = settings.html_filter() {
  106. let parts = split_command!(filter_invocation);
  107. let (cmd, args) = (parts[0], &parts[1..]);
  108. let command_obj = Command::new(cmd)
  109. .args(args)
  110. .stdin(Stdio::piped())
  111. .stdout(Stdio::piped())
  112. .spawn();
  113. if command_obj.is_err() {
  114. context.replies.push_back(UIEvent::Notification(
  115. Some(format!(
  116. "Failed to start html filter process: {}",
  117. filter_invocation,
  118. )),
  119. String::new(),
  120. ));
  121. return;
  122. }
  123. let mut html_filter = command_obj.unwrap();
  124. html_filter
  125. .stdin
  126. .as_mut()
  127. .unwrap()
  128. .write_all(&v)
  129. .expect("Failed to write to stdin");
  130. *v = format!(
  131. "Text piped through `{}`. Press `v` to open in web browser. \n\n",
  132. filter_invocation
  133. )
  134. .into_bytes();
  135. v.extend(html_filter.wait_with_output().unwrap().stdout);
  136. } else {
  137. if let Ok(mut html_filter) = Command::new("w3m")
  138. .args(&["-I", "utf-8", "-T", "text/html"])
  139. .stdin(Stdio::piped())
  140. .stdout(Stdio::piped())
  141. .spawn()
  142. {
  143. html_filter
  144. .stdin
  145. .as_mut()
  146. .unwrap()
  147. .write_all(&v)
  148. .expect("Failed to write to html filter stdin");
  149. *v = String::from(
  150. "Text piped through `w3m`. Press `v` to open in web browser. \n\n",
  151. )
  152. .into_bytes();
  153. v.extend(html_filter.wait_with_output().unwrap().stdout);
  154. } else {
  155. context.replies.push_back(UIEvent::Notification(
  156. Some(
  157. "Failed to find any application to use as html filter"
  158. .to_string(),
  159. ),
  160. String::new(),
  161. ));
  162. return;
  163. }
  164. }
  165. }
  166. })),
  167. ))
  168. .into_owned();
  169. match self.mode {
  170. ViewMode::Normal | ViewMode::Subview => {
  171. let mut t = body_text.to_string();
  172. t.push('\n');
  173. if body.count_attachments() > 1 {
  174. t = body
  175. .attachments()
  176. .iter()
  177. .enumerate()
  178. .fold(t, |mut s, (idx, a)| {
  179. s.push_str(&format!("\n[{}] {}\n", idx, a));
  180. s
  181. });
  182. }
  183. t
  184. }
  185. ViewMode::Raw => String::from_utf8_lossy(body.bytes()).into_owned(),
  186. ViewMode::Url => {
  187. let mut t = body_text.to_string();
  188. for (lidx, l) in finder.links(&body.text()).enumerate() {
  189. let offset = if lidx < 10 {
  190. lidx * 3
  191. } else if lidx < 100 {
  192. 26 + (lidx - 9) * 4
  193. } else if lidx < 1000 {
  194. 385 + (lidx - 99) * 5
  195. } else {
  196. panic!("FIXME: Message body with more than 100 urls, fix this");
  197. };
  198. t.insert_str(l.start() + offset, &format!("[{}]", lidx));
  199. }
  200. if body.count_attachments() > 1 {
  201. t = body
  202. .attachments()
  203. .iter()
  204. .enumerate()
  205. .fold(t, |mut s, (idx, a)| {
  206. s.push_str(&format!("[{}] {}\n\n", idx, a));
  207. s
  208. });
  209. }
  210. t
  211. }
  212. ViewMode::Attachment(aidx) => {
  213. let attachments = body.attachments();
  214. let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
  215. ret.push_str(&attachments[aidx].text());
  216. ret
  217. }
  218. ViewMode::ContactSelector(_) => unimplemented!(),
  219. }
  220. }
  221. pub fn plain_text_to_buf(s: &str, highlight_urls: bool) -> CellBuffer {
  222. let mut buf = CellBuffer::from(s);
  223. if highlight_urls {
  224. let lines: Vec<&str> = s.split('\n').map(|l| l.trim_right()).collect();
  225. let mut shift = 0;
  226. let mut lidx_total = 0;
  227. let finder = LinkFinder::new();
  228. for r in &lines {
  229. for l in finder.links(&r) {
  230. let offset = if lidx_total < 10 {
  231. 3
  232. } else if lidx_total < 100 {
  233. 4
  234. } else if lidx_total < 1000 {
  235. 5
  236. } else {
  237. panic!("BUG: Message body with more than 100 urls");
  238. };
  239. for i in 1..=offset {
  240. buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226));
  241. //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226));
  242. //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226));
  243. }
  244. lidx_total += 1;
  245. }
  246. // Each Cell represents one char so next line will be:
  247. shift += r.chars().count() + 1;
  248. }
  249. }
  250. buf
  251. }
  252. pub fn update(&mut self, new_coordinates: (usize, usize, EnvelopeHash)) {
  253. self.coordinates = new_coordinates;
  254. self.mode = ViewMode::Normal;
  255. self.set_dirty();
  256. }
  257. }
  258. impl Component for MailView {
  259. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  260. if !self.is_dirty() {
  261. return;
  262. }
  263. let upper_left = upper_left!(area);
  264. let bottom_right = bottom_right!(area);
  265. let y: usize = {
  266. let account = &mut context.accounts[self.coordinates.0];
  267. if !account.contains_key(self.coordinates.2) {
  268. /* The envelope has been renamed or removed, so wait for the appropriate event to
  269. * arrive */
  270. return;
  271. }
  272. let (hash, is_seen) = {
  273. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  274. (envelope.hash(), envelope.is_seen())
  275. };
  276. if !is_seen {
  277. let op = account.operation(hash);
  278. let envelope: &mut Envelope = &mut account.get_env_mut(&self.coordinates.2);
  279. envelope.set_seen(op).unwrap();
  280. }
  281. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  282. if self.mode == ViewMode::Raw {
  283. clear_area(grid, area);
  284. context.dirty_areas.push_back(area);
  285. get_y(upper_left) - 1
  286. } else {
  287. let (x, y) = write_string_to_grid(
  288. &format!("Date: {}", envelope.date_as_str()),
  289. grid,
  290. Color::Byte(33),
  291. Color::Default,
  292. area,
  293. true,
  294. );
  295. for x in x..=get_x(bottom_right) {
  296. grid[(x, y)].set_ch(' ');
  297. grid[(x, y)].set_bg(Color::Default);
  298. grid[(x, y)].set_fg(Color::Default);
  299. }
  300. let (x, y) = write_string_to_grid(
  301. &format!("From: {}", envelope.field_from_to_string()),
  302. grid,
  303. Color::Byte(33),
  304. Color::Default,
  305. (set_y(upper_left, y + 1), bottom_right),
  306. true,
  307. );
  308. for x in x..=get_x(bottom_right) {
  309. grid[(x, y)].set_ch(' ');
  310. grid[(x, y)].set_bg(Color::Default);
  311. grid[(x, y)].set_fg(Color::Default);
  312. }
  313. let (x, y) = write_string_to_grid(
  314. &format!("To: {}", envelope.field_to_to_string()),
  315. grid,
  316. Color::Byte(33),
  317. Color::Default,
  318. (set_y(upper_left, y + 1), bottom_right),
  319. true,
  320. );
  321. for x in x..=get_x(bottom_right) {
  322. grid[(x, y)].set_ch(' ');
  323. grid[(x, y)].set_bg(Color::Default);
  324. grid[(x, y)].set_fg(Color::Default);
  325. }
  326. let (x, y) = write_string_to_grid(
  327. &format!("Subject: {}", envelope.subject()),
  328. grid,
  329. Color::Byte(33),
  330. Color::Default,
  331. (set_y(upper_left, y + 1), bottom_right),
  332. true,
  333. );
  334. for x in x..=get_x(bottom_right) {
  335. grid[(x, y)].set_ch(' ');
  336. grid[(x, y)].set_bg(Color::Default);
  337. grid[(x, y)].set_fg(Color::Default);
  338. }
  339. let (x, mut y) = write_string_to_grid(
  340. &format!("Message-ID: <{}>", envelope.message_id_raw()),
  341. grid,
  342. Color::Byte(33),
  343. Color::Default,
  344. (set_y(upper_left, y + 1), bottom_right),
  345. true,
  346. );
  347. for x in x..=get_x(bottom_right) {
  348. grid[(x, y)].set_ch(' ');
  349. grid[(x, y)].set_bg(Color::Default);
  350. grid[(x, y)].set_fg(Color::Default);
  351. }
  352. if self.expand_headers && envelope.in_reply_to().is_some() {
  353. let (x, _y) = write_string_to_grid(
  354. &format!("In-Reply-To: {}", envelope.in_reply_to_display().unwrap()),
  355. grid,
  356. Color::Byte(33),
  357. Color::Default,
  358. (set_y(upper_left, y + 1), bottom_right),
  359. true,
  360. );
  361. for x in x..=get_x(bottom_right) {
  362. grid[(x, _y)].set_ch(' ');
  363. grid[(x, _y)].set_bg(Color::Default);
  364. grid[(x, _y)].set_fg(Color::Default);
  365. }
  366. let (x, _y) = write_string_to_grid(
  367. &format!(
  368. "References: {}",
  369. envelope
  370. .references()
  371. .iter()
  372. .map(std::string::ToString::to_string)
  373. .collect::<Vec<String>>()
  374. .join(", ")
  375. ),
  376. grid,
  377. Color::Byte(33),
  378. Color::Default,
  379. (set_y(upper_left, _y + 1), bottom_right),
  380. true,
  381. );
  382. for x in x..=get_x(bottom_right) {
  383. grid[(x, _y)].set_ch(' ');
  384. grid[(x, _y)].set_bg(Color::Default);
  385. grid[(x, _y)].set_fg(Color::Default);
  386. }
  387. y = _y;
  388. }
  389. clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 1)));
  390. context
  391. .dirty_areas
  392. .push_back((upper_left, set_y(bottom_right, y + 1)));
  393. y + 1
  394. }
  395. };
  396. if self.dirty {
  397. let body = {
  398. let account = &mut context.accounts[self.coordinates.0];
  399. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  400. let op = account.operation(envelope.hash());
  401. envelope.body(op)
  402. };
  403. match self.mode {
  404. ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
  405. self.pager = None;
  406. let attachment = &body.attachments()[aidx];
  407. self.subview = Some(Box::new(HtmlView::new(
  408. &attachment,
  409. context,
  410. self.coordinates.0,
  411. )));
  412. self.mode = ViewMode::Subview;
  413. }
  414. ViewMode::Normal if body.is_html() => {
  415. self.subview =
  416. Some(Box::new(HtmlView::new(&body, context, self.coordinates.0)));
  417. self.pager = None;
  418. self.mode = ViewMode::Subview;
  419. }
  420. ViewMode::Subview | ViewMode::ContactSelector(_) => {}
  421. ViewMode::Raw => {
  422. let text = {
  423. let account = &mut context.accounts[self.coordinates.0];
  424. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  425. let mut op = account.operation(envelope.hash());
  426. op.as_bytes()
  427. .map(|v| String::from_utf8_lossy(v).into_owned())
  428. .unwrap_or_else(|e| e.to_string())
  429. };
  430. self.pager = Some(Pager::from_string(
  431. text,
  432. Some(context),
  433. None,
  434. Some(width!(area)),
  435. ));
  436. self.subview = None;
  437. }
  438. _ => {
  439. let text = {
  440. self.attachment_to_text(&body, context)
  441. /*
  442. // URL indexes must be colored (ugh..)
  443. MailView::plain_text_to_buf(&text, self.mode == ViewMode::Url)
  444. */
  445. };
  446. let cursor_pos = if self.mode.is_attachment() {
  447. Some(0)
  448. } else {
  449. self.pager.as_mut().map(|p| p.cursor_pos())
  450. };
  451. self.pager = Some(Pager::from_string(
  452. text,
  453. Some(context),
  454. cursor_pos,
  455. Some(width!(area)),
  456. ));
  457. self.subview = None;
  458. }
  459. };
  460. self.dirty = false;
  461. }
  462. match self.mode {
  463. ViewMode::Subview if self.subview.is_some() => {
  464. if let Some(s) = self.subview.as_mut() {
  465. s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  466. }
  467. }
  468. ViewMode::ContactSelector(ref mut s) => {
  469. clear_area(grid, (set_y(upper_left, y + 1), bottom_right));
  470. s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  471. }
  472. _ => {
  473. if let Some(p) = self.pager.as_mut() {
  474. p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  475. }
  476. }
  477. }
  478. }
  479. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  480. match self.mode {
  481. ViewMode::Subview => {
  482. if let Some(s) = self.subview.as_mut() {
  483. if s.process_event(event, context) {
  484. return true;
  485. }
  486. }
  487. }
  488. ViewMode::ContactSelector(ref mut s) => {
  489. if s.process_event(event, context) {
  490. return true;
  491. }
  492. }
  493. _ => {
  494. if let Some(p) = self.pager.as_mut() {
  495. if p.process_event(event, context) {
  496. return true;
  497. }
  498. }
  499. }
  500. }
  501. match *event {
  502. UIEvent::Input(Key::Char('c')) => {
  503. if let ViewMode::ContactSelector(_) = self.mode {
  504. if let ViewMode::ContactSelector(s) =
  505. std::mem::replace(&mut self.mode, ViewMode::Normal)
  506. {
  507. let account = &mut context.accounts[self.coordinates.0];
  508. let mut results = Vec::new();
  509. {
  510. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  511. for c in s.collect() {
  512. let c = usize::from_ne_bytes({
  513. [c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]
  514. });
  515. for (idx, env) in envelope
  516. .from()
  517. .iter()
  518. .chain(envelope.to().iter())
  519. .enumerate()
  520. {
  521. if idx != c {
  522. continue;
  523. }
  524. let mut new_card: Card = Card::new();
  525. new_card.set_email(env.get_email());
  526. new_card.set_name(env.get_display_name());
  527. results.push(new_card);
  528. }
  529. }
  530. }
  531. for c in results {
  532. account.address_book.add_card(c);
  533. }
  534. }
  535. return true;
  536. }
  537. let account = &mut context.accounts[self.coordinates.0];
  538. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  539. let mut entries = Vec::new();
  540. for (idx, env) in envelope
  541. .from()
  542. .iter()
  543. .chain(envelope.to().iter())
  544. .enumerate()
  545. {
  546. entries.push((idx.to_ne_bytes().to_vec(), format!("{}", env)));
  547. }
  548. self.mode = ViewMode::ContactSelector(Selector::new(entries, true));
  549. self.dirty = true;
  550. }
  551. UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) => {
  552. self.cmd_buf.clear();
  553. context
  554. .replies
  555. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  556. }
  557. UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
  558. self.cmd_buf.push(c);
  559. context
  560. .replies
  561. .push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
  562. self.cmd_buf.clone(),
  563. )));
  564. }
  565. UIEvent::Input(Key::Alt('r'))
  566. if self.mode == ViewMode::Normal || self.mode == ViewMode::Subview =>
  567. {
  568. self.mode = ViewMode::Raw;
  569. self.set_dirty();
  570. }
  571. UIEvent::Input(Key::Char('r'))
  572. if self.mode.is_attachment()
  573. || self.mode == ViewMode::Subview
  574. || self.mode == ViewMode::Url
  575. || self.mode == ViewMode::Raw =>
  576. {
  577. self.mode = ViewMode::Normal;
  578. self.set_dirty();
  579. }
  580. UIEvent::Input(Key::Char('a'))
  581. if !self.cmd_buf.is_empty()
  582. && (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
  583. {
  584. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  585. self.cmd_buf.clear();
  586. context
  587. .replies
  588. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  589. {
  590. let account = &mut context.accounts[self.coordinates.0];
  591. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  592. let op = account.operation(envelope.hash());
  593. if let Some(u) = envelope.body(op).attachments().get(lidx) {
  594. match u.content_type() {
  595. ContentType::MessageRfc822 => {
  596. self.mode = ViewMode::Subview;
  597. match EnvelopeWrapper::new(u.bytes().to_vec()) {
  598. Ok(wrapper) => {
  599. self.subview = Some(Box::new(EnvelopeView::new(
  600. wrapper,
  601. None,
  602. None,
  603. self.coordinates.0,
  604. )));
  605. }
  606. Err(e) => {
  607. context.replies.push_back(UIEvent::StatusEvent(
  608. StatusEvent::DisplayMessage(format!("{}", e)),
  609. ));
  610. }
  611. }
  612. return true;
  613. }
  614. ContentType::Text { .. } => {
  615. self.mode = ViewMode::Attachment(lidx);
  616. self.dirty = true;
  617. }
  618. ContentType::Multipart { .. } => {
  619. context.replies.push_back(UIEvent::StatusEvent(
  620. StatusEvent::DisplayMessage(
  621. "Multipart attachments are not supported yet.".to_string(),
  622. ),
  623. ));
  624. return true;
  625. }
  626. ContentType::Unsupported { .. } => {
  627. let attachment_type = u.mime_type();
  628. let binary = query_default_app(&attachment_type);
  629. if let Ok(binary) = binary {
  630. let p = create_temp_file(&decode(u, None), None);
  631. Command::new(&binary)
  632. .arg(p.path())
  633. .stdin(Stdio::piped())
  634. .stdout(Stdio::piped())
  635. .spawn()
  636. .unwrap_or_else(|_| {
  637. panic!("Failed to start {}", binary.display())
  638. });
  639. context.temp_files.push(p);
  640. } else {
  641. context.replies.push_back(UIEvent::StatusEvent(
  642. StatusEvent::DisplayMessage(format!(
  643. "Couldn't find a default application for type {}",
  644. attachment_type
  645. )),
  646. ));
  647. return true;
  648. }
  649. }
  650. ContentType::PGPSignature => {
  651. context.replies.push_back(UIEvent::StatusEvent(
  652. StatusEvent::DisplayMessage(
  653. "Signatures aren't supported yet".to_string(),
  654. ),
  655. ));
  656. return true;
  657. }
  658. }
  659. } else {
  660. context.replies.push_back(UIEvent::StatusEvent(
  661. StatusEvent::DisplayMessage(format!(
  662. "Attachment `{}` not found.",
  663. lidx
  664. )),
  665. ));
  666. return true;
  667. }
  668. };
  669. }
  670. UIEvent::Input(Key::Char('h')) => {
  671. self.expand_headers = !self.expand_headers;
  672. self.dirty = true;
  673. return true;
  674. }
  675. UIEvent::Input(Key::Char('g'))
  676. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
  677. {
  678. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  679. self.cmd_buf.clear();
  680. context
  681. .replies
  682. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  683. let url = {
  684. let account = &mut context.accounts[self.coordinates.0];
  685. let envelope: &Envelope = &account.get_env(&self.coordinates.2);
  686. let finder = LinkFinder::new();
  687. let op = account.operation(envelope.hash());
  688. let t = envelope.body(op).text().to_string();
  689. let links: Vec<Link> = finder.links(&t).collect();
  690. if let Some(u) = links.get(lidx) {
  691. u.as_str().to_string()
  692. } else {
  693. context.replies.push_back(UIEvent::StatusEvent(
  694. StatusEvent::DisplayMessage(format!("Link `{}` not found.", lidx)),
  695. ));
  696. return true;
  697. }
  698. };
  699. Command::new("xdg-open")
  700. .arg(url)
  701. .stdin(Stdio::piped())
  702. .stdout(Stdio::piped())
  703. .spawn()
  704. .expect("Failed to start xdg_open");
  705. }
  706. UIEvent::Input(Key::Char('u')) => {
  707. match self.mode {
  708. ViewMode::Normal => self.mode = ViewMode::Url,
  709. ViewMode::Url => self.mode = ViewMode::Normal,
  710. _ => {}
  711. }
  712. self.dirty = true;
  713. }
  714. UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => {
  715. self.coordinates.2 = new_hash;
  716. }
  717. _ => {
  718. return false;
  719. }
  720. }
  721. true
  722. }
  723. fn is_dirty(&self) -> bool {
  724. self.dirty
  725. || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  726. || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  727. || if let ViewMode::ContactSelector(ref s) = self.mode {
  728. s.is_dirty()
  729. } else {
  730. false
  731. }
  732. }
  733. fn set_dirty(&mut self) {
  734. self.dirty = true;
  735. match self.mode {
  736. ViewMode::Normal => {
  737. if let Some(p) = self.pager.as_mut() {
  738. p.set_dirty();
  739. }
  740. }
  741. ViewMode::Subview => {
  742. if let Some(s) = self.subview.as_mut() {
  743. s.set_dirty();
  744. }
  745. }
  746. _ => {}
  747. }
  748. }
  749. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  750. let mut map = if let Some(ref sbv) = self.subview {
  751. sbv.get_shortcuts(context)
  752. } else if let Some(ref pgr) = self.pager {
  753. pgr.get_shortcuts(context)
  754. } else {
  755. Default::default()
  756. };
  757. let mut our_map = FnvHashMap::with_capacity_and_hasher(4, Default::default());
  758. our_map.insert("add_addresses_to_contacts", Key::Char('c'));
  759. our_map.insert("view_raw_source", Key::Alt('r'));
  760. if self.mode.is_attachment() || self.mode == ViewMode::Subview || self.mode == ViewMode::Raw
  761. {
  762. our_map.insert("return_to_normal_view", Key::Char('r'));
  763. }
  764. our_map.insert("open_attachment", Key::Char('a'));
  765. if self.mode == ViewMode::Url {
  766. our_map.insert("go_to_url", Key::Char('g'));
  767. }
  768. if self.mode == ViewMode::Normal || self.mode == ViewMode::Url {
  769. our_map.insert("toggle_url_mode", Key::Char('u'));
  770. }
  771. map.insert(MailView::DESCRIPTION.to_string(), our_map);
  772. map
  773. }
  774. fn id(&self) -> ComponentId {
  775. self.id
  776. }
  777. fn set_id(&mut self, id: ComponentId) {
  778. self.id = id;
  779. }
  780. }