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.

688 lines
23KB

  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 melib::Draft;
  23. use std::str::FromStr;
  24. #[derive(Debug, PartialEq)]
  25. enum Cursor {
  26. Headers,
  27. Body,
  28. //Attachments,
  29. }
  30. #[derive(Debug)]
  31. pub struct Composer {
  32. reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, thread_node_index)
  33. account_cursor: usize,
  34. cursor: Cursor,
  35. pager: Pager,
  36. draft: Draft,
  37. form: FormWidget,
  38. mode: ViewMode,
  39. dirty: bool,
  40. initialized: bool,
  41. id: ComponentId,
  42. }
  43. impl Default for Composer {
  44. fn default() -> Self {
  45. Composer {
  46. reply_context: None,
  47. account_cursor: 0,
  48. cursor: Cursor::Headers,
  49. pager: Pager::default(),
  50. draft: Draft::default(),
  51. form: FormWidget::default(),
  52. mode: ViewMode::Edit,
  53. dirty: true,
  54. initialized: false,
  55. id: ComponentId::new_v4(),
  56. }
  57. }
  58. }
  59. #[derive(Debug)]
  60. enum ViewMode {
  61. Discard(Uuid),
  62. Edit,
  63. //Selector(Selector),
  64. Overview,
  65. }
  66. impl ViewMode {
  67. fn is_discard(&self) -> bool {
  68. if let ViewMode::Discard(_) = self {
  69. true
  70. } else {
  71. false
  72. }
  73. }
  74. fn is_edit(&self) -> bool {
  75. if let ViewMode::Edit = self {
  76. true
  77. } else {
  78. false
  79. }
  80. }
  81. fn is_overview(&self) -> bool {
  82. if let ViewMode::Overview = self {
  83. true
  84. } else {
  85. false
  86. }
  87. }
  88. }
  89. impl fmt::Display for Composer {
  90. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  91. // TODO display subject/info
  92. if self.reply_context.is_some() {
  93. write!(f, "reply: {:8}", self.draft.headers()["Subject"])
  94. } else {
  95. write!(f, "compose")
  96. }
  97. }
  98. }
  99. impl Composer {
  100. const DESCRIPTION: &'static str = "compose";
  101. pub fn new(account_cursor: usize) -> Self {
  102. Composer {
  103. account_cursor,
  104. id: ComponentId::new_v4(),
  105. ..Default::default()
  106. }
  107. }
  108. /*
  109. * coordinates: (account index, mailbox index, root set thread_node index)
  110. * msg: index of message we reply to in thread_nodes
  111. * context: current context
  112. */
  113. pub fn edit(account_pos: usize, h: EnvelopeHash, context: &Context) -> Self {
  114. let mut ret = Composer::default();
  115. let op = context.accounts[account_pos].operation(h);
  116. let envelope: &Envelope = context.accounts[account_pos].get_env(&h);
  117. ret.draft = Draft::edit(envelope, op);
  118. ret.account_cursor = account_pos;
  119. ret
  120. }
  121. pub fn with_context(
  122. coordinates: (usize, usize, usize),
  123. msg: ThreadHash,
  124. context: &Context,
  125. ) -> Self {
  126. let account = &context.accounts[coordinates.0];
  127. let mailbox = &account[coordinates.1].as_ref().unwrap();
  128. let threads = &account.collection.threads[&mailbox.folder.hash()];
  129. let thread_nodes = &threads.thread_nodes();
  130. let mut ret = Composer::default();
  131. let p = &thread_nodes[&msg];
  132. let parent_message = &account.collection[&p.message().unwrap()];
  133. let mut op = account.operation(parent_message.hash());
  134. let parent_bytes = op.as_bytes();
  135. ret.draft = Draft::new_reply(parent_message, parent_bytes.unwrap());
  136. ret.draft.headers_mut().insert(
  137. "Subject".into(),
  138. if p.show_subject() {
  139. format!(
  140. "Re: {}",
  141. account.get_env(&p.message().unwrap()).subject().clone()
  142. )
  143. } else {
  144. account.get_env(&p.message().unwrap()).subject().into()
  145. },
  146. );
  147. ret.account_cursor = coordinates.0;
  148. ret.reply_context = Some((
  149. (coordinates.1, coordinates.2),
  150. Box::new(ThreadView::new(coordinates, Some(msg), context)),
  151. ));
  152. ret
  153. }
  154. fn update_draft(&mut self) {
  155. let header_values = self.form.values_mut();
  156. let draft_header_map = self.draft.headers_mut();
  157. /* avoid extra allocations by updating values instead of inserting */
  158. for (k, v) in draft_header_map.iter_mut() {
  159. if let Some(vn) = header_values.remove(k) {
  160. std::mem::swap(v, &mut vn.into_string());
  161. }
  162. }
  163. }
  164. fn update_form(&mut self) {
  165. let old_cursor = self.form.cursor();
  166. self.form = FormWidget::new("Save".into());
  167. self.form.hide_buttons();
  168. self.form.set_cursor(old_cursor);
  169. let headers = self.draft.headers();
  170. let account_cursor = self.account_cursor;
  171. for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
  172. if k == "To" || k == "Cc" || k == "Bcc" {
  173. self.form.push_cl((
  174. k.into(),
  175. headers[k].to_string(),
  176. Box::new(move |c, term| {
  177. let book: &AddressBook = &c.accounts[account_cursor].address_book;
  178. let results: Vec<String> = book.search(term);
  179. results
  180. }),
  181. ));
  182. } else {
  183. self.form.push((k.into(), headers[k].to_string()));
  184. }
  185. }
  186. }
  187. /*
  188. let (x, y) = if k == "From" {
  189. write_string_to_grid(
  190. "◀ ",
  191. grid,
  192. Color::Byte(251),
  193. Color::Default,
  194. ((x, y), set_y(bottom_right, y)),
  195. true,
  196. )
  197. } else {
  198. (x, y)
  199. };
  200. let (x, y) = write_string_to_grid(
  201. &headers[k],
  202. grid,
  203. Color::Default,
  204. bg_color,
  205. ((x, y), set_y(bottom_right, y)),
  206. true,
  207. );
  208. if k == "From" {
  209. write_string_to_grid(
  210. " ▶",
  211. grid,
  212. Color::Byte(251),
  213. Color::Default,
  214. ((x, y), set_y(bottom_right, y)),
  215. true,
  216. )
  217. */
  218. }
  219. impl Component for Composer {
  220. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  221. clear_area(grid, area);
  222. let upper_left = upper_left!(area);
  223. let bottom_right = bottom_right!(area);
  224. let upper_left = set_y(upper_left, get_y(upper_left) + 1);
  225. let width = if width!(area) > 80 && self.reply_context.is_some() {
  226. width!(area) / 2
  227. } else {
  228. width!(area)
  229. };
  230. if !self.initialized {
  231. if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
  232. {
  233. self.draft.headers_mut().insert(
  234. "From".into(),
  235. get_display_name(context, self.account_cursor),
  236. );
  237. }
  238. self.pager.update_from_str(self.draft.body(), Some(77));
  239. self.update_form();
  240. self.initialized = true;
  241. }
  242. let header_height = self.form.len() + 1;
  243. let mid = if width > 80 {
  244. let width = width - 80;
  245. let mid = if self.reply_context.is_some() {
  246. get_x(upper_left) + width!(area) / 2
  247. } else {
  248. width / 2
  249. };
  250. if self.reply_context.is_some() {
  251. for i in get_y(upper_left) - 1..=get_y(bottom_right) {
  252. set_and_join_box(grid, (mid, i), VERT_BOUNDARY);
  253. grid[(mid, i)].set_fg(Color::Default);
  254. grid[(mid, i)].set_bg(Color::Default);
  255. }
  256. grid[set_x(bottom_right, mid)].set_ch(VERT_BOUNDARY); // Enforce full vert bar at the bottom
  257. grid[set_x(bottom_right, mid)].set_fg(Color::Byte(240));
  258. }
  259. if self.dirty {
  260. for i in get_y(upper_left)..=get_y(bottom_right) {
  261. //set_and_join_box(grid, (mid, i), VERT_BOUNDARY);
  262. grid[(mid, i)].set_fg(Color::Default);
  263. grid[(mid, i)].set_bg(Color::Default);
  264. //set_and_join_box(grid, (mid + 80, i), VERT_BOUNDARY);
  265. grid[(mid + 80, i)].set_fg(Color::Default);
  266. grid[(mid + 80, i)].set_bg(Color::Default);
  267. }
  268. }
  269. mid
  270. } else {
  271. 0
  272. };
  273. if width > 80 && self.reply_context.is_some() {
  274. let area = (pos_dec(upper_left, (0, 1)), set_x(bottom_right, mid - 1));
  275. let view = &mut self.reply_context.as_mut().unwrap().1;
  276. view.set_dirty();
  277. view.draw(grid, area, context);
  278. }
  279. let header_area = if self.reply_context.is_some() {
  280. (
  281. set_x(upper_left, mid + 1),
  282. set_y(bottom_right, get_y(upper_left) + header_height + 1),
  283. )
  284. } else {
  285. (
  286. set_x(upper_left, mid + 1),
  287. (
  288. get_x(bottom_right).saturating_sub(mid),
  289. get_y(upper_left) + header_height + 1,
  290. ),
  291. )
  292. };
  293. let body_area = if self.reply_context.is_some() {
  294. (
  295. (mid + 1, get_y(upper_left) + header_height + 1),
  296. bottom_right,
  297. )
  298. } else {
  299. (
  300. pos_inc(upper_left, (mid + 1, header_height + 2)),
  301. pos_dec(bottom_right, (mid, 0)),
  302. )
  303. };
  304. let (x, y) = write_string_to_grid(
  305. if self.reply_context.is_some() {
  306. "COMPOSING REPLY"
  307. } else {
  308. "COMPOSING MESSAGE"
  309. },
  310. grid,
  311. Color::Byte(189),
  312. Color::Byte(167),
  313. (
  314. pos_dec(upper_left!(header_area), (0, 1)),
  315. bottom_right!(header_area),
  316. ),
  317. false,
  318. );
  319. change_colors(
  320. grid,
  321. (
  322. set_x(pos_dec(upper_left!(header_area), (0, 1)), x),
  323. set_y(bottom_right!(header_area), y),
  324. ),
  325. Color::Byte(189),
  326. Color::Byte(167),
  327. );
  328. /* Regardless of view mode, do the following */
  329. self.form.draw(grid, header_area, context);
  330. match self.mode {
  331. ViewMode::Overview | ViewMode::Edit => {
  332. self.pager.set_dirty();
  333. self.pager.draw(grid, body_area, context);
  334. }
  335. ViewMode::Discard(_) => {
  336. /* Let user choose whether to quit with/without saving or cancel */
  337. let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
  338. let mid_y = { std::cmp::max(height!(area) / 2, 11) - 11 };
  339. let upper_left = upper_left!(body_area);
  340. let bottom_right = bottom_right!(body_area);
  341. let area = (
  342. pos_inc(upper_left, (mid_x, mid_y)),
  343. pos_dec(bottom_right, (mid_x, mid_y)),
  344. );
  345. create_box(grid, area);
  346. let area = (
  347. pos_inc(upper_left, (mid_x + 2, mid_y + 2)),
  348. pos_dec(
  349. bottom_right,
  350. (mid_x.saturating_sub(2), mid_y.saturating_sub(2)),
  351. ),
  352. );
  353. let (_, y) = write_string_to_grid(
  354. &format!("Draft \"{:10}\"", self.draft.headers()["Subject"]),
  355. grid,
  356. Color::Default,
  357. Color::Default,
  358. area,
  359. true,
  360. );
  361. let (_, y) = write_string_to_grid(
  362. "[x] quit without saving",
  363. grid,
  364. Color::Byte(124),
  365. Color::Default,
  366. (set_y(upper_left!(area), y + 2), bottom_right!(area)),
  367. true,
  368. );
  369. let (_, y) = write_string_to_grid(
  370. "[y] save draft and quit",
  371. grid,
  372. Color::Byte(124),
  373. Color::Default,
  374. (set_y(upper_left!(area), y + 1), bottom_right!(area)),
  375. true,
  376. );
  377. write_string_to_grid(
  378. "[n] cancel",
  379. grid,
  380. Color::Byte(124),
  381. Color::Default,
  382. (set_y(upper_left!(area), y + 1), bottom_right!(area)),
  383. true,
  384. );
  385. }
  386. }
  387. context.dirty_areas.push_back(area);
  388. }
  389. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  390. match (&mut self.mode, &mut self.reply_context, &event) {
  391. // don't pass Reply command to thread view in reply_context
  392. (_, _, UIEvent::Input(Key::Char('R'))) => {}
  393. (ViewMode::Overview, Some((_, ref mut view)), _) => {
  394. if view.process_event(event, context) {
  395. self.dirty = true;
  396. return true;
  397. }
  398. /* Cannot mutably borrow in pattern guard, pah! */
  399. if self.pager.process_event(event, context) {
  400. return true;
  401. }
  402. }
  403. (ViewMode::Overview, _, _) => {
  404. /* Cannot mutably borrow in pattern guard, pah! */
  405. if self.pager.process_event(event, context) {
  406. return true;
  407. }
  408. }
  409. _ => {}
  410. }
  411. if self.form.process_event(event, context) {
  412. return true;
  413. }
  414. match *event {
  415. UIEvent::Resize => {
  416. self.set_dirty();
  417. }
  418. /*
  419. /* Switch e-mail From: field to the `left` configured account. */
  420. UIEvent::Input(Key::Left) if self.cursor == Cursor::From => {
  421. self.account_cursor = self.account_cursor.saturating_sub(1);
  422. self.draft.headers_mut().insert(
  423. "From".into(),
  424. get_display_name(context, self.account_cursor),
  425. );
  426. self.dirty = true;
  427. return true;
  428. }
  429. /* Switch e-mail From: field to the `right` configured account. */
  430. UIEvent::Input(Key::Right) if self.cursor == Cursor::From => {
  431. if self.account_cursor + 1 < context.accounts.len() {
  432. self.account_cursor += 1;
  433. self.draft.headers_mut().insert(
  434. "From".into(),
  435. get_display_name(context, self.account_cursor),
  436. );
  437. self.dirty = true;
  438. }
  439. return true;
  440. }*/
  441. UIEvent::Input(Key::Up) => {
  442. self.cursor = Cursor::Headers;
  443. }
  444. UIEvent::Input(Key::Down) => {
  445. self.cursor = Cursor::Body;
  446. }
  447. UIEvent::Input(Key::Char(key)) if self.mode.is_discard() => {
  448. match (key, &self.mode) {
  449. ('x', ViewMode::Discard(u)) => {
  450. context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
  451. return true;
  452. }
  453. ('n', _) => {}
  454. ('y', ViewMode::Discard(u)) => {
  455. let account = &context.accounts[self.account_cursor];
  456. let draft = std::mem::replace(&mut self.draft, Draft::default());
  457. if let Err(e) = account.save_draft(draft) {
  458. debug!("{:?} could not save draft", e);
  459. context.replies.push_back(UIEvent::Notification(
  460. Some("Could not save draft.".into()),
  461. e.into(),
  462. ));
  463. }
  464. context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
  465. return true;
  466. }
  467. _ => {
  468. return false;
  469. }
  470. }
  471. self.mode = ViewMode::Overview;
  472. self.set_dirty();
  473. return true;
  474. }
  475. /* Switch to Overview mode if we're on Edit mode */
  476. UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => {
  477. self.mode = ViewMode::Overview;
  478. self.set_dirty();
  479. return true;
  480. }
  481. /* Switch to Edit mode if we're on Overview mode */
  482. UIEvent::Input(Key::Char('o')) if self.mode.is_overview() => {
  483. self.mode = ViewMode::Edit;
  484. self.set_dirty();
  485. return true;
  486. }
  487. UIEvent::Input(Key::Char('s')) if self.mode.is_overview() => {
  488. use std::io::Write;
  489. use std::process::{Command, Stdio};
  490. let settings = &context.settings;
  491. let parts = split_command!(settings.mailer.mailer_cmd);
  492. let (cmd, args) = (parts[0], &parts[1..]);
  493. let mut msmtp = Command::new(cmd)
  494. .args(args)
  495. .stdin(Stdio::piped())
  496. .stdout(Stdio::piped())
  497. .spawn()
  498. .expect("Failed to start mailer command");
  499. {
  500. let stdin = msmtp.stdin.as_mut().expect("failed to open stdin");
  501. self.update_draft();
  502. let draft = self.draft.clone().finalise().unwrap();
  503. stdin
  504. .write_all(draft.as_bytes())
  505. .expect("Failed to write to stdin");
  506. if let Err(e) = context.accounts[self.account_cursor].save(
  507. draft.as_bytes(),
  508. &context.accounts[self.account_cursor]
  509. .settings
  510. .conf()
  511. .sent_folder(),
  512. ) {
  513. debug!("{:?} could not save sent msg", e);
  514. context.replies.push_back(UIEvent::Notification(
  515. Some("Could not save in 'Sent' folder.".into()),
  516. e.into(),
  517. ));
  518. }
  519. }
  520. context.replies.push_back(UIEvent::Notification(
  521. Some("Sent.".into()),
  522. format!(
  523. "Mailer output: {:#?}",
  524. msmtp
  525. .wait_with_output()
  526. .expect("Failed to wait on filter")
  527. .stdout
  528. ),
  529. ));
  530. context
  531. .replies
  532. .push_back(UIEvent::Action(Tab(Kill(self.id))));
  533. return true;
  534. }
  535. UIEvent::Input(Key::Char('e')) if self.cursor == Cursor::Body => {
  536. /* Edit draft in $EDITOR */
  537. use std::process::{Command, Stdio};
  538. /* Kill input thread so that spawned command can be sole receiver of stdin */
  539. {
  540. context.input_kill();
  541. }
  542. /* update Draft's headers based on form values */
  543. self.update_draft();
  544. let f = create_temp_file(self.draft.to_string().unwrap().as_str().as_bytes(), None);
  545. //let mut f = Box::new(std::fs::File::create(&dir).unwrap());
  546. // TODO: check exit status
  547. Command::new("vim")
  548. .arg("+/^$")
  549. .arg(&f.path())
  550. .stdin(Stdio::inherit())
  551. .stdout(Stdio::inherit())
  552. .output()
  553. .expect("failed to execute process");
  554. let result = f.read_to_string();
  555. self.draft = Draft::from_str(result.as_str()).unwrap();
  556. self.initialized = false;
  557. context.replies.push_back(UIEvent::Fork(ForkType::Finished));
  558. context.restore_input();
  559. /*
  560. Cursor::To | Cursor::Cc | Cursor::Bcc => {
  561. let account = &context.accounts[self.account_cursor];
  562. let mut entries = account.address_book.values().map(|v| (v.id().as_bytes().to_vec(), v.email().to_string())).collect();
  563. self.mode = ViewMode::Selector(Selector::new(entries, true));
  564. },
  565. Cursor::Attachments => {
  566. unimplemented!()
  567. },
  568. Cursor::From => {
  569. return true;
  570. }
  571. */
  572. self.dirty = true;
  573. return true;
  574. }
  575. _ => {}
  576. }
  577. false
  578. }
  579. fn is_dirty(&self) -> bool {
  580. self.dirty
  581. || self.pager.is_dirty()
  582. || self
  583. .reply_context
  584. .as_ref()
  585. .map(|(_, p)| p.is_dirty())
  586. .unwrap_or(false)
  587. || self.form.is_dirty()
  588. }
  589. fn set_dirty(&mut self) {
  590. self.dirty = true;
  591. self.pager.set_dirty();
  592. self.form.set_dirty();
  593. if let Some((_, ref mut view)) = self.reply_context {
  594. view.set_dirty();
  595. }
  596. }
  597. fn kill(&mut self, uuid: Uuid) {
  598. self.mode = ViewMode::Discard(uuid);
  599. }
  600. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  601. let mut map = if self.mode.is_overview() {
  602. self.pager.get_shortcuts(context)
  603. } else {
  604. Default::default()
  605. };
  606. if let Some((_, ref view)) = self.reply_context {
  607. map.extend(view.get_shortcuts(context));
  608. }
  609. let mut our_map: ShortcutMap = Default::default();
  610. if self.mode.is_overview() {
  611. our_map.insert("Switch to edit mode.", Key::Char('o'));
  612. our_map.insert("Deliver draft to mailer.", Key::Char('s'));
  613. }
  614. if self.mode.is_edit() {
  615. our_map.insert("Switch to overview", Key::Char('v'));
  616. }
  617. our_map.insert("Edit in $EDITOR", Key::Char('e'));
  618. map.insert(Composer::DESCRIPTION.to_string(), our_map);
  619. map
  620. }
  621. fn id(&self) -> ComponentId {
  622. self.id
  623. }
  624. fn set_id(&mut self, id: ComponentId) {
  625. self.id = id;
  626. }
  627. }
  628. fn get_display_name(context: &Context, idx: usize) -> String {
  629. let settings = context.accounts[idx].runtime_settings.account();
  630. if let Some(d) = settings.display_name.as_ref() {
  631. format!("{} <{}>", d, settings.identity)
  632. } else {
  633. settings.identity.to_string()
  634. }
  635. }