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.

620 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. mod compact;
  23. pub use self::compact::*;
  24. mod thread;
  25. pub use self::thread::*;
  26. mod plain;
  27. pub use self::plain::*;
  28. #[derive(Debug)]
  29. struct AccountMenuEntry {
  30. name: String,
  31. // Index in the config account vector.
  32. index: usize,
  33. }
  34. trait ListingTrait {
  35. fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>);
  36. fn set_coordinates(&mut self, _: (usize, usize, Option<EnvelopeHash>));
  37. }
  38. #[derive(Debug)]
  39. pub enum ListingComponent {
  40. Plain(PlainListing),
  41. Threaded(ThreadListing),
  42. Compact(CompactListing),
  43. }
  44. use crate::ListingComponent::*;
  45. impl ListingTrait for ListingComponent {
  46. fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>) {
  47. match &self {
  48. Compact(ref l) => l.coordinates(),
  49. Plain(ref l) => l.coordinates(),
  50. Threaded(ref l) => l.coordinates(),
  51. }
  52. }
  53. fn set_coordinates(&mut self, c: (usize, usize, Option<EnvelopeHash>)) {
  54. match self {
  55. Compact(ref mut l) => l.set_coordinates(c),
  56. Plain(ref mut l) => l.set_coordinates(c),
  57. Threaded(ref mut l) => l.set_coordinates(c),
  58. }
  59. }
  60. }
  61. #[derive(Debug)]
  62. pub struct Listing {
  63. component: ListingComponent,
  64. accounts: Vec<AccountMenuEntry>,
  65. dirty: bool,
  66. visible: bool,
  67. cursor_pos: (usize, usize),
  68. id: ComponentId,
  69. show_divider: bool,
  70. menu_visibility: bool,
  71. /// This is the width of the right container to the entire width.
  72. ratio: usize, // right/(container width) * 100
  73. }
  74. impl fmt::Display for Listing {
  75. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  76. match self.component {
  77. Compact(ref l) => write!(f, "{}", l),
  78. Plain(ref l) => write!(f, "{}", l),
  79. Threaded(ref l) => write!(f, "{}", l),
  80. }
  81. }
  82. }
  83. impl Component for Listing {
  84. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  85. if !self.is_dirty() {
  86. return;
  87. }
  88. if !is_valid_area!(area) {
  89. return;
  90. }
  91. let upper_left = upper_left!(area);
  92. let bottom_right = bottom_right!(area);
  93. let total_cols = get_x(bottom_right) - get_x(upper_left);
  94. let right_component_width = if self.menu_visibility {
  95. (self.ratio * total_cols) / 100
  96. } else {
  97. total_cols
  98. };
  99. let mid = get_x(bottom_right) - right_component_width;
  100. if self.dirty && mid != get_x(upper_left) {
  101. if self.show_divider {
  102. for i in get_y(upper_left)..=get_y(bottom_right) {
  103. grid[(mid, i)].set_ch(VERT_BOUNDARY);
  104. grid[(mid, i)].set_fg(Color::Default);
  105. grid[(mid, i)].set_bg(Color::Default);
  106. }
  107. } else {
  108. for i in get_y(upper_left)..=get_y(bottom_right) {
  109. grid[(mid, i)].set_fg(Color::Default);
  110. grid[(mid, i)].set_bg(Color::Default);
  111. }
  112. }
  113. context
  114. .dirty_areas
  115. .push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right))));
  116. }
  117. if right_component_width == total_cols {
  118. match self.component {
  119. Compact(ref mut l) => l.draw(grid, area, context),
  120. Plain(ref mut l) => l.draw(grid, area, context),
  121. Threaded(ref mut l) => l.draw(grid, area, context),
  122. }
  123. } else if right_component_width == 0 {
  124. self.draw_menu(grid, area, context);
  125. } else {
  126. self.draw_menu(grid, (upper_left, (mid, get_y(bottom_right))), context);
  127. match self.component {
  128. Compact(ref mut l) => {
  129. l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context)
  130. }
  131. Plain(ref mut l) => {
  132. l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context)
  133. }
  134. Threaded(ref mut l) => {
  135. l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context)
  136. }
  137. }
  138. }
  139. }
  140. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  141. if match self.component {
  142. Plain(ref mut l) => l.process_event(event, context),
  143. Compact(ref mut l) => l.process_event(event, context),
  144. Threaded(ref mut l) => l.process_event(event, context),
  145. } {
  146. return true;
  147. }
  148. let shortcuts = &self.get_shortcuts(context)[Listing::DESCRIPTION];
  149. match *event {
  150. UIEvent::Input(ref k)
  151. if k == shortcuts["next_folder"] || k == shortcuts["prev_folder"] =>
  152. {
  153. let folder_length = context.accounts[self.cursor_pos.0].len();
  154. match k {
  155. k if k == shortcuts["next_folder"] && folder_length > 0 => {
  156. if self.cursor_pos.1 < folder_length - 1 {
  157. self.cursor_pos.1 += 1;
  158. self.component.set_coordinates((
  159. self.cursor_pos.0,
  160. self.cursor_pos.1,
  161. None,
  162. ));
  163. self.set_dirty();
  164. } else {
  165. return true;
  166. }
  167. }
  168. k if k == shortcuts["prev_folder"] => {
  169. if self.cursor_pos.1 > 0 {
  170. self.cursor_pos.1 -= 1;
  171. self.component.set_coordinates((
  172. self.cursor_pos.0,
  173. self.cursor_pos.1,
  174. None,
  175. ));
  176. self.set_dirty();
  177. } else {
  178. return true;
  179. }
  180. }
  181. _ => return false,
  182. }
  183. let folder_hash =
  184. context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1];
  185. // Inform State that we changed the current folder view.
  186. context
  187. .replies
  188. .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash)));
  189. return true;
  190. }
  191. UIEvent::Input(ref k)
  192. if k == shortcuts["next_account"] || k == shortcuts["prev_account"] =>
  193. {
  194. match k {
  195. k if k == shortcuts["next_account"] => {
  196. if self.cursor_pos.0 < self.accounts.len() - 1 {
  197. self.cursor_pos = (self.cursor_pos.0 + 1, 0);
  198. self.component.set_coordinates((self.cursor_pos.0, 0, None));
  199. self.set_dirty();
  200. } else {
  201. return true;
  202. }
  203. }
  204. k if k == shortcuts["prev_account"] => {
  205. if self.cursor_pos.0 > 0 {
  206. self.cursor_pos = (self.cursor_pos.0 - 1, 0);
  207. self.component.set_coordinates((self.cursor_pos.0, 0, None));
  208. self.set_dirty();
  209. } else {
  210. return true;
  211. }
  212. }
  213. _ => return false,
  214. }
  215. let folder_hash =
  216. context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1];
  217. // Inform State that we changed the current folder view.
  218. context
  219. .replies
  220. .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash)));
  221. return true;
  222. }
  223. UIEvent::Action(ref action) => match action {
  224. Action::Listing(ListingAction::SetPlain) => {
  225. if let Plain(_) = self.component {
  226. return true;
  227. }
  228. let mut new_l = PlainListing::default();
  229. new_l.set_coordinates((self.cursor_pos.0, self.cursor_pos.1, None));
  230. self.component = Plain(new_l);
  231. return true;
  232. }
  233. Action::Listing(ListingAction::SetThreaded) => {
  234. if let Threaded(_) = self.component {
  235. return true;
  236. }
  237. let mut new_l = ThreadListing::default();
  238. new_l.set_coordinates((self.cursor_pos.0, self.cursor_pos.1, None));
  239. self.component = Threaded(new_l);
  240. return true;
  241. }
  242. Action::Listing(ListingAction::SetCompact) => {
  243. if let Compact(_) = self.component {
  244. return true;
  245. }
  246. let mut new_l = CompactListing::default();
  247. new_l.set_coordinates((self.cursor_pos.0, self.cursor_pos.1, None));
  248. self.component = Compact(new_l);
  249. return true;
  250. }
  251. _ => {}
  252. },
  253. UIEvent::RefreshMailbox((idxa, folder_hash)) => {
  254. self.cursor_pos = (
  255. idxa,
  256. context.accounts[idxa]
  257. .folders_order
  258. .iter()
  259. .position(|&h| h == folder_hash)
  260. .unwrap_or(0),
  261. );
  262. self.dirty = true;
  263. }
  264. UIEvent::ChangeMode(UIMode::Normal) => {
  265. self.dirty = true;
  266. }
  267. UIEvent::Resize => {
  268. self.dirty = true;
  269. }
  270. UIEvent::Input(ref k) if k == shortcuts["toggle-menu-visibility"] => {
  271. self.menu_visibility = !self.menu_visibility;
  272. self.set_dirty();
  273. }
  274. UIEvent::Input(ref k) if k == shortcuts["new_mail"] => {
  275. context
  276. .replies
  277. .push_back(UIEvent::Action(Tab(NewDraft(self.cursor_pos.0))));
  278. return true;
  279. }
  280. UIEvent::StartupCheck(_) => {
  281. self.dirty = true;
  282. }
  283. UIEvent::MailboxUpdate(_) => {
  284. self.dirty = true;
  285. }
  286. _ => {}
  287. }
  288. false
  289. }
  290. fn is_dirty(&self) -> bool {
  291. self.dirty
  292. || match self.component {
  293. Compact(ref l) => l.is_dirty(),
  294. Plain(ref l) => l.is_dirty(),
  295. Threaded(ref l) => l.is_dirty(),
  296. }
  297. }
  298. fn set_dirty(&mut self) {
  299. self.dirty = true;
  300. match self.component {
  301. Compact(ref mut l) => l.set_dirty(),
  302. Plain(ref mut l) => l.set_dirty(),
  303. Threaded(ref mut l) => l.set_dirty(),
  304. }
  305. }
  306. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  307. let mut map = match self.component {
  308. Compact(ref l) => l.get_shortcuts(context),
  309. Plain(ref l) => l.get_shortcuts(context),
  310. Threaded(ref l) => l.get_shortcuts(context),
  311. };
  312. let config_map = context.settings.shortcuts.listing.key_values();
  313. map.insert(
  314. Listing::DESCRIPTION.to_string(),
  315. [
  316. (
  317. "new_mail",
  318. if let Some(key) = config_map.get("new_mail") {
  319. (*key).clone()
  320. } else {
  321. Key::Char('m')
  322. },
  323. ),
  324. (
  325. "prev_folder",
  326. if let Some(key) = config_map.get("prev_folder") {
  327. (*key).clone()
  328. } else {
  329. Key::Char('K')
  330. },
  331. ),
  332. (
  333. "next_folder",
  334. if let Some(key) = config_map.get("next_folder") {
  335. (*key).clone()
  336. } else {
  337. Key::Char('J')
  338. },
  339. ),
  340. (
  341. "prev_account",
  342. if let Some(key) = config_map.get("prev_account") {
  343. (*key).clone()
  344. } else {
  345. Key::Char('l')
  346. },
  347. ),
  348. (
  349. "next_account",
  350. if let Some(key) = config_map.get("next_account") {
  351. (*key).clone()
  352. } else {
  353. Key::Char('h')
  354. },
  355. ),
  356. ("toggle-menu-visibility", Key::Char('`')),
  357. ]
  358. .iter()
  359. .cloned()
  360. .collect(),
  361. );
  362. map
  363. }
  364. fn id(&self) -> ComponentId {
  365. match self.component {
  366. Compact(ref l) => l.id(),
  367. Plain(ref l) => l.id(),
  368. Threaded(ref l) => l.id(),
  369. }
  370. }
  371. fn set_id(&mut self, id: ComponentId) {
  372. match self.component {
  373. Compact(ref mut l) => l.set_id(id),
  374. Plain(ref mut l) => l.set_id(id),
  375. Threaded(ref mut l) => l.set_id(id),
  376. }
  377. }
  378. }
  379. impl From<IndexStyle> for ListingComponent {
  380. fn from(index_style: IndexStyle) -> Self {
  381. match index_style {
  382. IndexStyle::Plain => Plain(Default::default()),
  383. IndexStyle::Threaded => Threaded(Default::default()),
  384. IndexStyle::Compact => Compact(Default::default()),
  385. }
  386. }
  387. }
  388. impl Listing {
  389. const DESCRIPTION: &'static str = "listing";
  390. pub fn new(accounts: &[Account]) -> Self {
  391. let accounts = accounts
  392. .iter()
  393. .enumerate()
  394. .map(|(i, a)| AccountMenuEntry {
  395. name: a.name().to_string(),
  396. index: i,
  397. })
  398. .collect();
  399. Listing {
  400. component: Compact(Default::default()),
  401. accounts,
  402. visible: true,
  403. dirty: true,
  404. cursor_pos: (0, 0),
  405. id: ComponentId::new_v4(),
  406. show_divider: false,
  407. menu_visibility: true,
  408. ratio: 90,
  409. }
  410. }
  411. fn draw_menu(&mut self, grid: &mut CellBuffer, mut area: Area, context: &mut Context) {
  412. if !self.is_dirty() {
  413. return;
  414. }
  415. clear_area(grid, area);
  416. /* visually divide menu and listing */
  417. area = (area.0, pos_dec(area.1, (1, 0)));
  418. let upper_left = upper_left!(area);
  419. let bottom_right = bottom_right!(area);
  420. self.dirty = false;
  421. let mut y = get_y(upper_left);
  422. for a in &self.accounts {
  423. y += 1;
  424. y += self.print_account(grid, (set_y(upper_left, y), bottom_right), &a, context);
  425. }
  426. context.dirty_areas.push_back(area);
  427. }
  428. /*
  429. * Print a single account in the menu area.
  430. */
  431. fn print_account(
  432. &self,
  433. grid: &mut CellBuffer,
  434. area: Area,
  435. a: &AccountMenuEntry,
  436. context: &mut Context,
  437. ) -> usize {
  438. if !is_valid_area!(area) {
  439. debug!("BUG: invalid area in print_account");
  440. }
  441. // Each entry and its index in the account
  442. let entries: FnvHashMap<FolderHash, Folder> = context.accounts[a.index]
  443. .list_folders()
  444. .into_iter()
  445. .map(|f| (f.hash(), f))
  446. .collect();
  447. let folders_order: FnvHashMap<FolderHash, usize> = context.accounts[a.index]
  448. .folders_order()
  449. .iter()
  450. .enumerate()
  451. .map(|(i, &fh)| (fh, i))
  452. .collect();
  453. let upper_left = upper_left!(area);
  454. let bottom_right = bottom_right!(area);
  455. let highlight = self.cursor_pos.0 == a.index;
  456. let mut inc = 0;
  457. let mut depth = String::from("");
  458. let mut s = format!("{}\n", a.name);
  459. fn pop(depth: &mut String) {
  460. depth.pop();
  461. }
  462. fn push(depth: &mut String, c: char) {
  463. depth.push(c);
  464. }
  465. fn print(
  466. folder_idx: FolderHash,
  467. depth: &mut String,
  468. inc: &mut usize,
  469. entries: &FnvHashMap<FolderHash, Folder>,
  470. folders_order: &FnvHashMap<FolderHash, usize>,
  471. s: &mut String,
  472. index: usize, //account index
  473. context: &mut Context,
  474. ) {
  475. match context.accounts[index].status(entries[&folder_idx].hash()) {
  476. Ok(_) => {
  477. let account = &context.accounts[index];
  478. let count = account[entries[&folder_idx].hash()]
  479. .as_ref()
  480. .unwrap()
  481. .envelopes
  482. .iter()
  483. .map(|h| &account.collection[&h])
  484. .filter(|e| !e.is_seen())
  485. .count();
  486. let len = s.len();
  487. s.insert_str(
  488. len,
  489. &format!("{} {} {}\n ", *inc, &entries[&folder_idx].name(), count),
  490. );
  491. }
  492. Err(_) => {
  493. let len = s.len();
  494. s.insert_str(
  495. len,
  496. &format!("{} {} ...\n ", *inc, &entries[&folder_idx].name()),
  497. );
  498. }
  499. }
  500. *inc += 1;
  501. let mut children: Vec<FolderHash> = entries[&folder_idx].children().to_vec();
  502. children
  503. .sort_unstable_by(|a, b| folders_order[a].partial_cmp(&folders_order[b]).unwrap());
  504. push(depth, ' ');
  505. for child in children {
  506. let len = s.len();
  507. s.insert_str(len, &format!("{} ", depth));
  508. print(child, depth, inc, entries, folders_order, s, index, context);
  509. }
  510. pop(depth);
  511. }
  512. for f in entries.keys() {
  513. if entries[f].parent().is_none() {
  514. print(
  515. *f,
  516. &mut depth,
  517. &mut inc,
  518. &entries,
  519. &folders_order,
  520. &mut s,
  521. a.index,
  522. context,
  523. );
  524. }
  525. }
  526. let lines: Vec<&str> = s.lines().collect();
  527. let lines_len = lines.len();
  528. if lines_len < 2 {
  529. return 0;
  530. }
  531. let mut idx = 0;
  532. for y in get_y(upper_left)..get_y(bottom_right) {
  533. if idx == lines_len {
  534. break;
  535. }
  536. let s = lines[idx].to_string();
  537. let (color_fg, color_bg) = if highlight {
  538. if self.cursor_pos.1 + 1 == idx {
  539. (Color::Byte(233), Color::Byte(15))
  540. } else {
  541. (Color::Byte(15), Color::Byte(233))
  542. }
  543. } else {
  544. (Color::Default, Color::Default)
  545. };
  546. write_string_to_grid(
  547. &s,
  548. grid,
  549. color_fg,
  550. color_bg,
  551. (set_y(upper_left, y), bottom_right),
  552. false,
  553. );
  554. {
  555. enum CellPos {
  556. BeforeIndex,
  557. Index,
  558. //AfterIndex,
  559. }
  560. let mut pos = CellPos::BeforeIndex;
  561. let mut x = get_x(upper_left);
  562. while let Some(cell) = grid.get_mut(x, y) {
  563. if x == get_x(bottom_right) {
  564. break;
  565. }
  566. match (cell.ch(), &pos) {
  567. (c, CellPos::Index) | (c, CellPos::BeforeIndex) if c.is_numeric() => {
  568. pos = CellPos::Index;
  569. cell.set_fg(Color::Byte(243));
  570. x += 1;
  571. continue;
  572. }
  573. (c, CellPos::BeforeIndex) if c.is_whitespace() => {
  574. x += 1;
  575. continue;
  576. }
  577. _ => {
  578. break;
  579. }
  580. }
  581. }
  582. }
  583. idx += 1;
  584. }
  585. if idx == 0 {
  586. 0
  587. } else {
  588. idx - 1
  589. }
  590. }
  591. }