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.

668 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 std::cmp;
  23. use std::ops::{Deref, DerefMut};
  24. const MAX_COLS: usize = 500;
  25. macro_rules! address_list {
  26. (($name:expr) as comma_sep_list) => {{
  27. let mut ret: String =
  28. $name
  29. .into_iter()
  30. .fold(String::new(), |mut s: String, n: &Address| {
  31. s.extend(n.to_string().chars());
  32. s.push_str(", ");
  33. s
  34. });
  35. ret.pop();
  36. ret.pop();
  37. ret
  38. }};
  39. }
  40. macro_rules! column_str {
  41. (
  42. struct $name:ident(String)) => {
  43. pub struct $name(String);
  44. impl Deref for $name {
  45. type Target = String;
  46. fn deref(&self) -> &String {
  47. &self.0
  48. }
  49. }
  50. impl DerefMut for $name {
  51. fn deref_mut(&mut self) -> &mut String {
  52. &mut self.0
  53. }
  54. }
  55. };
  56. }
  57. column_str!(struct IndexNoString(String));
  58. column_str!(struct DateString(String));
  59. column_str!(struct FromString(String));
  60. column_str!(struct SubjectString(String));
  61. /// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a
  62. /// `MailView`.
  63. #[derive(Debug)]
  64. pub struct PlainListing {
  65. /// (x, y, z): x is accounts, y is folders, z is index inside a folder.
  66. cursor_pos: (usize, usize, usize),
  67. new_cursor_pos: (usize, usize, usize),
  68. length: usize,
  69. local_collection: Vec<EnvelopeHash>,
  70. sort: (SortField, SortOrder),
  71. subsort: (SortField, SortOrder),
  72. /// Cache current view.
  73. content: CellBuffer,
  74. /// If we must redraw on next redraw event
  75. dirty: bool,
  76. /// If `self.view` exists or not.
  77. unfocused: bool,
  78. view: Option<MailView>,
  79. movement: Option<PageMovement>,
  80. id: ComponentId,
  81. }
  82. impl ListingTrait for PlainListing {
  83. fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>) {
  84. (self.cursor_pos.0, self.cursor_pos.1, None)
  85. }
  86. fn set_coordinates(&mut self, coordinates: (usize, usize, Option<EnvelopeHash>)) {
  87. self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
  88. }
  89. }
  90. impl Default for PlainListing {
  91. fn default() -> Self {
  92. Self::new()
  93. }
  94. }
  95. impl fmt::Display for PlainListing {
  96. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  97. write!(f, "mail")
  98. }
  99. }
  100. impl PlainListing {
  101. /// Helper function to format entry strings for PlainListing */
  102. /* TODO: Make this configurable */
  103. fn make_entry_string(
  104. e: &Envelope,
  105. idx: usize,
  106. ) -> (IndexNoString, FromString, DateString, SubjectString) {
  107. (
  108. IndexNoString(idx.to_string()),
  109. FromString(address_list!((e.from()) as comma_sep_list)),
  110. DateString(PlainListing::format_date(e)),
  111. SubjectString(format!(
  112. "{}{}",
  113. e.subject(),
  114. if e.has_attachments() { " 📎" } else { "" },
  115. )),
  116. )
  117. }
  118. pub fn new() -> Self {
  119. let content = CellBuffer::new(0, 0, Cell::with_char(' '));
  120. PlainListing {
  121. cursor_pos: (0, 1, 0),
  122. new_cursor_pos: (0, 0, 0),
  123. length: 0,
  124. local_collection: Vec::new(),
  125. sort: (Default::default(), Default::default()),
  126. subsort: (Default::default(), Default::default()),
  127. content,
  128. dirty: true,
  129. unfocused: false,
  130. view: None,
  131. movement: None,
  132. id: ComponentId::new_v4(),
  133. }
  134. }
  135. /// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has
  136. /// chosen.
  137. fn refresh_mailbox(&mut self, context: &mut Context) {
  138. self.dirty = true;
  139. if !(self.cursor_pos.0 == self.new_cursor_pos.0
  140. && self.cursor_pos.1 == self.new_cursor_pos.1)
  141. {
  142. self.cursor_pos.2 = 0;
  143. self.new_cursor_pos.2 = 0;
  144. }
  145. self.cursor_pos.0 = self.new_cursor_pos.0;
  146. self.cursor_pos.1 = self.new_cursor_pos.1;
  147. let folder_hash = context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1];
  148. // Inform State that we changed the current folder view.
  149. context
  150. .replies
  151. .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash)));
  152. // Get mailbox as a reference.
  153. //
  154. match context.accounts[self.cursor_pos.0].status(folder_hash) {
  155. Ok(_) => {}
  156. Err(_) => {
  157. self.content = CellBuffer::new(MAX_COLS, 1, Cell::with_char(' '));
  158. self.length = 0;
  159. write_string_to_grid(
  160. "Loading.",
  161. &mut self.content,
  162. Color::Default,
  163. Color::Default,
  164. ((0, 0), (MAX_COLS - 1, 0)),
  165. false,
  166. );
  167. return;
  168. }
  169. }
  170. let account = &context.accounts[self.cursor_pos.0];
  171. let mailbox = &account[self.cursor_pos.1].as_ref().unwrap();
  172. self.length = mailbox.len();
  173. self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
  174. if self.length == 0 {
  175. write_string_to_grid(
  176. &format!("Folder `{}` is empty.", mailbox.folder.name()),
  177. &mut self.content,
  178. Color::Default,
  179. Color::Default,
  180. ((0, 0), (MAX_COLS - 1, 0)),
  181. true,
  182. );
  183. return;
  184. }
  185. self.local_collection = account.collection.keys().cloned().collect();
  186. let sort = self.sort;
  187. self.local_collection.sort_by(|a, b| match sort {
  188. (SortField::Date, SortOrder::Desc) => {
  189. let ma = &account.get_env(a);
  190. let mb = &account.get_env(b);
  191. mb.date().cmp(&ma.date())
  192. }
  193. (SortField::Date, SortOrder::Asc) => {
  194. let ma = &account.get_env(a);
  195. let mb = &account.get_env(b);
  196. ma.date().cmp(&mb.date())
  197. }
  198. (SortField::Subject, SortOrder::Desc) => {
  199. let ma = &account.get_env(a);
  200. let mb = &account.get_env(b);
  201. ma.subject().cmp(&mb.subject())
  202. }
  203. (SortField::Subject, SortOrder::Asc) => {
  204. let ma = &account.get_env(a);
  205. let mb = &account.get_env(b);
  206. mb.subject().cmp(&ma.subject())
  207. }
  208. });
  209. let mut rows = Vec::with_capacity(1024);
  210. let mut min_width = (0, 0, 0);
  211. let widths: (usize, usize, usize);
  212. for idx in 0..self.local_collection.len() {
  213. let envelope: &Envelope = &account.get_env(&self.local_collection[idx]);
  214. let strings = PlainListing::make_entry_string(envelope, idx);
  215. min_width.0 = cmp::max(min_width.0, strings.0.len()); /* index */
  216. min_width.1 = cmp::max(min_width.1, strings.2.split_graphemes().len()); /* date */
  217. min_width.2 = cmp::max(min_width.2, strings.3.split_graphemes().len()); /* subject */
  218. rows.push(strings);
  219. }
  220. let column_sep: usize = if MAX_COLS >= min_width.0 + min_width.1 + min_width.2 {
  221. widths = min_width;
  222. 2
  223. } else {
  224. let width = MAX_COLS - 3 - min_width.0;
  225. widths = (
  226. min_width.0,
  227. cmp::min(min_width.1, width / 3),
  228. cmp::min(min_width.2, width / 3),
  229. );
  230. 1
  231. };
  232. // Populate `CellBuffer` with every entry.
  233. for (idx, y) in (0..=self.length).enumerate() {
  234. if idx >= self.length {
  235. /* No more entries left, so fill the rest of the area with empty space */
  236. clear_area(&mut self.content, ((0, y), (MAX_COLS - 1, self.length)));
  237. break;
  238. }
  239. /* Write an entire line for each envelope entry. */
  240. let envelope: &Envelope = &account.get_env(&self.local_collection[idx]);
  241. let fg_color = if !envelope.is_seen() {
  242. Color::Byte(0)
  243. } else {
  244. Color::Default
  245. };
  246. let bg_color = if !envelope.is_seen() {
  247. Color::Byte(251)
  248. } else if idx % 2 == 0 {
  249. Color::Byte(236)
  250. } else {
  251. Color::Default
  252. };
  253. let (x, _) = write_string_to_grid(
  254. &rows[idx].0,
  255. &mut self.content,
  256. fg_color,
  257. bg_color,
  258. ((0, idx), (widths.0, idx)),
  259. false,
  260. );
  261. for x in x..=widths.0 + column_sep {
  262. self.content[(x, idx)].set_bg(bg_color);
  263. }
  264. let mut _x = widths.0 + column_sep;
  265. let (x, _) = write_string_to_grid(
  266. &rows[idx].2,
  267. &mut self.content,
  268. fg_color,
  269. bg_color,
  270. ((_x, idx), (widths.1 + _x, idx)),
  271. false,
  272. );
  273. _x += widths.1 + column_sep + 1;
  274. for x in x.._x {
  275. self.content[(x, idx)].set_bg(bg_color);
  276. }
  277. let (x, _) = write_string_to_grid(
  278. &rows[idx].1,
  279. &mut self.content,
  280. fg_color,
  281. bg_color,
  282. ((_x, idx), (widths.1 + _x, idx)),
  283. false,
  284. );
  285. _x += widths.1 + column_sep + 2;
  286. for x in x.._x {
  287. self.content[(x, idx)].set_bg(bg_color);
  288. }
  289. let (x, _) = write_string_to_grid(
  290. &rows[idx].3,
  291. &mut self.content,
  292. fg_color,
  293. bg_color,
  294. ((_x, idx), (widths.2 + _x, idx)),
  295. false,
  296. );
  297. for x in x..MAX_COLS {
  298. self.content[(x, y)].set_ch(' ');
  299. self.content[(x, y)].set_bg(bg_color);
  300. }
  301. }
  302. }
  303. fn unhighlight_line(&mut self, idx: usize) {
  304. let fg_color = Color::Default;
  305. let bg_color = if idx % 2 == 0 {
  306. Color::Byte(236)
  307. } else {
  308. Color::Default
  309. };
  310. change_colors(
  311. &mut self.content,
  312. ((0, idx), (MAX_COLS - 1, idx)),
  313. fg_color,
  314. bg_color,
  315. );
  316. }
  317. fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
  318. let account = &context.accounts[self.cursor_pos.0];
  319. let envelope: &Envelope = &account.get_env(&self.local_collection[idx]);
  320. let fg_color = if !envelope.is_seen() {
  321. Color::Byte(0)
  322. } else {
  323. Color::Default
  324. };
  325. let bg_color = if self.cursor_pos.2 == idx {
  326. Color::Byte(246)
  327. } else if !envelope.is_seen() {
  328. Color::Byte(251)
  329. } else if idx % 2 == 0 {
  330. Color::Byte(236)
  331. } else {
  332. Color::Default
  333. };
  334. change_colors(grid, area, fg_color, bg_color);
  335. }
  336. /// Draw the list of `Envelope`s.
  337. fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  338. if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
  339. {
  340. self.refresh_mailbox(context);
  341. }
  342. let upper_left = upper_left!(area);
  343. let bottom_right = bottom_right!(area);
  344. if self.length == 0 {
  345. clear_area(grid, area);
  346. copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0)));
  347. context.dirty_areas.push_back(area);
  348. return;
  349. }
  350. let rows = get_y(bottom_right) - get_y(upper_left) + 1;
  351. if let Some(mvm) = self.movement.take() {
  352. match mvm {
  353. PageMovement::PageUp => {
  354. self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(rows);
  355. }
  356. PageMovement::PageDown => {
  357. if self.new_cursor_pos.2 + rows + 1 < self.length {
  358. self.new_cursor_pos.2 += rows;
  359. } else {
  360. self.new_cursor_pos.2 = (self.length / rows) * rows;
  361. }
  362. }
  363. PageMovement::Home => {
  364. self.new_cursor_pos.2 = 0;
  365. }
  366. PageMovement::End => {
  367. self.new_cursor_pos.2 = (self.length / rows) * rows;
  368. }
  369. }
  370. }
  371. let prev_page_no = (self.cursor_pos.2).wrapping_div(rows);
  372. let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
  373. let top_idx = page_no * rows;
  374. /* If cursor position has changed, remove the highlight from the previous position and
  375. * apply it in the new one. */
  376. if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no {
  377. let old_cursor_pos = self.cursor_pos;
  378. self.cursor_pos = self.new_cursor_pos;
  379. for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] {
  380. if *idx >= self.length {
  381. continue; //bounds check
  382. }
  383. let new_area = (
  384. set_y(upper_left, get_y(upper_left) + (*idx % rows)),
  385. set_y(bottom_right, get_y(upper_left) + (*idx % rows)),
  386. );
  387. self.highlight_line(grid, new_area, *idx, context);
  388. context.dirty_areas.push_back(new_area);
  389. }
  390. return;
  391. } else if self.cursor_pos != self.new_cursor_pos {
  392. self.cursor_pos = self.new_cursor_pos;
  393. }
  394. /* Page_no has changed, so draw new page */
  395. copy_area(
  396. grid,
  397. &self.content,
  398. area,
  399. ((0, top_idx), (MAX_COLS - 1, self.length)),
  400. );
  401. self.highlight_line(
  402. grid,
  403. (
  404. set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)),
  405. set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)),
  406. ),
  407. self.cursor_pos.2,
  408. context,
  409. );
  410. context.dirty_areas.push_back(area);
  411. }
  412. fn format_date(envelope: &Envelope) -> String {
  413. let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date());
  414. let now: std::time::Duration = std::time::SystemTime::now()
  415. .duration_since(d)
  416. .unwrap_or_else(|_| std::time::Duration::new(std::u64::MAX, 0));
  417. match now.as_secs() {
  418. n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)),
  419. n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)),
  420. n if n < 4 * 24 * 60 * 60 => {
  421. format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9))
  422. }
  423. _ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(),
  424. }
  425. }
  426. }
  427. impl Component for PlainListing {
  428. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  429. if !self.unfocused {
  430. if !self.is_dirty() {
  431. return;
  432. }
  433. self.dirty = false;
  434. /* Draw the entire list */
  435. self.draw_list(grid, area, context);
  436. } else {
  437. let upper_left = upper_left!(area);
  438. let bottom_right = bottom_right!(area);
  439. if self.length == 0 && self.dirty {
  440. clear_area(grid, area);
  441. context.dirty_areas.push_back(area);
  442. }
  443. /* Render the mail body in a pager, basically copy what HSplit does */
  444. let total_rows = get_y(bottom_right) - get_y(upper_left);
  445. let pager_ratio = context.runtime_settings.pager.pager_ratio;
  446. let bottom_entity_rows = (pager_ratio * total_rows) / 100;
  447. if bottom_entity_rows > total_rows {
  448. clear_area(grid, area);
  449. context.dirty_areas.push_back(area);
  450. return;
  451. }
  452. /* Mark message as read */
  453. let idx = self.cursor_pos.2;
  454. let must_unhighlight = {
  455. if self.length == 0 {
  456. false
  457. } else {
  458. let account = &mut context.accounts[self.cursor_pos.0];
  459. let envelope: &mut Envelope =
  460. &mut account.get_env_mut(&self.local_collection[idx]);
  461. !envelope.is_seen()
  462. }
  463. };
  464. if must_unhighlight {
  465. self.unhighlight_line(idx);
  466. }
  467. let mid = get_y(upper_left) + total_rows - bottom_entity_rows;
  468. self.draw_list(
  469. grid,
  470. (
  471. upper_left,
  472. (get_x(bottom_right), get_y(upper_left) + mid - 1),
  473. ),
  474. context,
  475. );
  476. if self.length == 0 {
  477. self.dirty = false;
  478. return;
  479. }
  480. {
  481. /* TODO: Move the box drawing business in separate functions */
  482. if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY
  483. {
  484. grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT);
  485. }
  486. for i in get_x(upper_left)..=get_x(bottom_right) {
  487. grid[(i, mid)].set_ch('─');
  488. }
  489. context
  490. .dirty_areas
  491. .push_back((set_y(upper_left, mid), set_y(bottom_right, mid)));
  492. }
  493. // TODO: Make headers view configurable
  494. if !self.dirty {
  495. if let Some(v) = self.view.as_mut() {
  496. v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context);
  497. }
  498. return;
  499. }
  500. {
  501. let coordinates = self.cursor_pos;
  502. let coordinates = (
  503. coordinates.0,
  504. coordinates.1,
  505. self.local_collection[self.cursor_pos.2],
  506. );
  507. self.view = Some(MailView::new(coordinates, None, None));
  508. }
  509. self.view.as_mut().unwrap().draw(
  510. grid,
  511. (set_y(upper_left, mid + 1), bottom_right),
  512. context,
  513. );
  514. self.dirty = false;
  515. }
  516. }
  517. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  518. if let Some(ref mut v) = self.view {
  519. if v.process_event(event, context) {
  520. return true;
  521. }
  522. }
  523. match *event {
  524. UIEvent::Input(Key::Up) => {
  525. if self.cursor_pos.2 > 0 {
  526. self.new_cursor_pos.2 -= 1;
  527. self.dirty = true;
  528. }
  529. return true;
  530. }
  531. UIEvent::Input(Key::Down) => {
  532. if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 {
  533. self.new_cursor_pos.2 += 1;
  534. self.dirty = true;
  535. }
  536. return true;
  537. }
  538. UIEvent::Input(ref key) if *key == Key::PageUp => {
  539. self.movement = Some(PageMovement::PageUp);
  540. self.set_dirty();
  541. }
  542. UIEvent::Input(ref key) if *key == Key::PageDown => {
  543. self.movement = Some(PageMovement::PageDown);
  544. self.set_dirty();
  545. }
  546. UIEvent::Input(ref key) if *key == Key::Home => {
  547. self.movement = Some(PageMovement::Home);
  548. self.set_dirty();
  549. }
  550. UIEvent::Input(ref key) if *key == Key::End => {
  551. self.movement = Some(PageMovement::End);
  552. self.set_dirty();
  553. }
  554. UIEvent::Input(Key::Char('\n')) if !self.unfocused => {
  555. self.unfocused = true;
  556. self.dirty = true;
  557. return true;
  558. }
  559. UIEvent::Input(Key::Char('i')) if self.unfocused => {
  560. self.unfocused = false;
  561. self.dirty = true;
  562. self.view = None;
  563. return true;
  564. }
  565. UIEvent::RefreshMailbox(_) => {
  566. self.dirty = true;
  567. self.view = None;
  568. }
  569. UIEvent::MailboxUpdate((ref idxa, ref idxf))
  570. if (*idxa, *idxf)
  571. == (
  572. self.new_cursor_pos.0,
  573. context.accounts[self.new_cursor_pos.0].folders_order
  574. [self.new_cursor_pos.1],
  575. ) =>
  576. {
  577. self.refresh_mailbox(context);
  578. self.set_dirty();
  579. }
  580. UIEvent::StartupCheck(ref f)
  581. if *f
  582. == context.accounts[self.new_cursor_pos.0].folders_order
  583. [self.new_cursor_pos.1] =>
  584. {
  585. self.refresh_mailbox(context);
  586. self.set_dirty();
  587. }
  588. UIEvent::ChangeMode(UIMode::Normal) => {
  589. self.dirty = true;
  590. }
  591. UIEvent::Resize => {
  592. self.dirty = true;
  593. }
  594. UIEvent::Action(ref action) => match action {
  595. Action::ViewMailbox(idx) => {
  596. self.new_cursor_pos.1 = *idx;
  597. self.dirty = true;
  598. self.refresh_mailbox(context);
  599. return true;
  600. }
  601. Action::SubSort(field, order) => {
  602. debug!("SubSort {:?} , {:?}", field, order);
  603. self.subsort = (*field, *order);
  604. self.dirty = true;
  605. self.refresh_mailbox(context);
  606. return true;
  607. }
  608. Action::Sort(field, order) => {
  609. debug!("Sort {:?} , {:?}", field, order);
  610. self.sort = (*field, *order);
  611. self.dirty = true;
  612. self.refresh_mailbox(context);
  613. return true;
  614. }
  615. _ => {}
  616. },
  617. _ => {}
  618. }
  619. false
  620. }
  621. fn is_dirty(&self) -> bool {
  622. self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  623. }
  624. fn set_dirty(&mut self) {
  625. if let Some(p) = self.view.as_mut() {
  626. p.set_dirty();
  627. };
  628. self.dirty = true;
  629. }
  630. fn id(&self) -> ComponentId {
  631. self.id
  632. }
  633. fn set_id(&mut self, id: ComponentId) {
  634. self.id = id;
  635. }
  636. }