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.
 
 
 
 
 

530 lines
19 KiB

  1. /*
  2. * meli - bin.rs
  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. //!
  22. //! This crate contains the frontend stuff of the application. The application entry way on
  23. //! `src/bin.rs` creates an event loop and passes input to a thread.
  24. //!
  25. //! The mail handling stuff is done in the `melib` crate which includes all backend needs. The
  26. //! split is done to theoretically be able to create different frontends with the same innards.
  27. //!
  28. use std::alloc::System;
  29. use std::collections::VecDeque;
  30. use std::path::PathBuf;
  31. extern crate notify_rust;
  32. extern crate xdg_utils;
  33. #[macro_use]
  34. extern crate serde_derive;
  35. extern crate linkify;
  36. extern crate uuid;
  37. extern crate bitflags;
  38. extern crate serde_json;
  39. #[macro_use]
  40. extern crate smallvec;
  41. extern crate termion;
  42. #[global_allocator]
  43. static GLOBAL: System = System;
  44. #[macro_use]
  45. extern crate melib;
  46. use melib::*;
  47. mod unix;
  48. use unix::*;
  49. #[macro_use]
  50. pub mod types;
  51. use crate::types::*;
  52. #[macro_use]
  53. pub mod terminal;
  54. use crate::terminal::*;
  55. #[macro_use]
  56. pub mod command;
  57. use crate::command::*;
  58. pub mod state;
  59. use crate::state::*;
  60. pub mod components;
  61. use crate::components::*;
  62. #[macro_use]
  63. pub mod conf;
  64. use crate::conf::*;
  65. pub mod workers;
  66. use crate::workers::*;
  67. #[cfg(feature = "sqlite3")]
  68. pub mod sqlite3;
  69. pub mod jobs;
  70. pub mod mailcap;
  71. pub mod plugins;
  72. use std::os::raw::c_int;
  73. fn notify(
  74. signals: &[c_int],
  75. sender: crossbeam::channel::Sender<ThreadEvent>,
  76. ) -> std::result::Result<crossbeam::channel::Receiver<c_int>, std::io::Error> {
  77. use std::time::Duration;
  78. let (alarm_pipe_r, alarm_pipe_w) = nix::unistd::pipe().map_err(|err| {
  79. std::io::Error::from_raw_os_error(err.as_errno().map(|n| n as i32).unwrap_or(0))
  80. })?;
  81. let alarm_handler = move |info: &nix::libc::siginfo_t| {
  82. let value = unsafe { info.si_value().sival_ptr as u8 };
  83. let _ = nix::unistd::write(alarm_pipe_w, &[value]);
  84. };
  85. unsafe {
  86. signal_hook_registry::register_sigaction(signal_hook::SIGALRM, alarm_handler)?;
  87. }
  88. let (s, r) = crossbeam::channel::bounded(100);
  89. let signals = signal_hook::iterator::Signals::new(signals)?;
  90. let _ = nix::fcntl::fcntl(
  91. alarm_pipe_r,
  92. nix::fcntl::FcntlArg::F_SETFL(nix::fcntl::OFlag::O_NONBLOCK),
  93. );
  94. std::thread::spawn(move || {
  95. let mut buf = [0; 1];
  96. let mut ctr = 0;
  97. loop {
  98. ctr %= 3;
  99. if ctr == 0 {
  100. let _ = sender
  101. .send_timeout(ThreadEvent::Pulse, Duration::from_millis(500))
  102. .ok();
  103. }
  104. for signal in signals.pending() {
  105. let _ = s.send_timeout(signal, Duration::from_millis(500)).ok();
  106. }
  107. while nix::unistd::read(alarm_pipe_r, buf.as_mut())
  108. .map(|s| s > 0)
  109. .unwrap_or(false)
  110. {
  111. let value = buf[0];
  112. let _ = sender.send_timeout(
  113. ThreadEvent::UIEvent(UIEvent::Timer(value)),
  114. Duration::from_millis(2000),
  115. );
  116. }
  117. std::thread::sleep(std::time::Duration::from_millis(100));
  118. ctr += 1;
  119. }
  120. });
  121. Ok(r)
  122. }
  123. fn parse_manpage(src: &str) -> Result<ManPages> {
  124. match src {
  125. "" | "meli" | "main" => Ok(ManPages::Main),
  126. "meli.conf" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
  127. "meli-themes" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
  128. _ => Err(MeliError::new(format!(
  129. "Invalid documentation page: {}",
  130. src
  131. ))),
  132. }
  133. }
  134. use structopt::StructOpt;
  135. #[derive(Debug)]
  136. /// Choose manpage
  137. enum ManPages {
  138. /// meli(1)
  139. Main = 0,
  140. /// meli.conf(5)
  141. Conf = 1,
  142. /// meli-themes(5)
  143. Themes = 2,
  144. }
  145. #[derive(Debug, StructOpt)]
  146. #[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
  147. struct Opt {
  148. /// use specified configuration file
  149. #[structopt(short, long, parse(from_os_str))]
  150. config: Option<PathBuf>,
  151. #[structopt(subcommand)]
  152. subcommand: Option<SubCommand>,
  153. }
  154. #[derive(Debug, StructOpt)]
  155. enum SubCommand {
  156. /// print default theme in full to stdout and exit.
  157. PrintDefaultTheme,
  158. /// print loaded themes in full to stdout and exit.
  159. PrintLoadedThemes,
  160. /// create a sample configuration file with available configuration options. If PATH is not specified, meli will try to create it in $XDG_CONFIG_HOME/meli/config.toml
  161. #[structopt(display_order = 1)]
  162. CreateConfig {
  163. #[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
  164. path: Option<PathBuf>,
  165. },
  166. /// test a configuration file for syntax issues or missing options.
  167. #[structopt(display_order = 2)]
  168. TestConfig {
  169. #[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
  170. path: Option<PathBuf>,
  171. },
  172. #[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
  173. #[structopt(display_order = 3)]
  174. /// print documentation page and exit (Piping to a pager is recommended.).
  175. Man(ManOpt),
  176. /// View mail from input file.
  177. View {
  178. #[structopt(value_name = "INPUT", parse(from_os_str))]
  179. path: PathBuf,
  180. },
  181. }
  182. #[derive(Debug, StructOpt)]
  183. struct ManOpt {
  184. #[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))]
  185. page: ManPages,
  186. }
  187. fn main() {
  188. let opt = Opt::from_args();
  189. ::std::process::exit(match run_app(opt) {
  190. Ok(()) => 0,
  191. Err(err) => {
  192. eprintln!("{}", err);
  193. 1
  194. }
  195. });
  196. }
  197. fn run_app(opt: Opt) -> Result<()> {
  198. if let Some(config_location) = opt.config.as_ref() {
  199. std::env::set_var("MELI_CONFIG", config_location);
  200. }
  201. match opt.subcommand {
  202. Some(SubCommand::TestConfig { path }) => {
  203. let config_path = if let Some(path) = path {
  204. path
  205. } else {
  206. crate::conf::get_config_file()?
  207. };
  208. conf::FileSettings::validate(config_path)?;
  209. return Ok(());
  210. }
  211. Some(SubCommand::CreateConfig { path }) => {
  212. let config_path = if let Some(path) = path {
  213. path
  214. } else {
  215. crate::conf::get_config_file()?
  216. };
  217. if config_path.exists() {
  218. return Err(MeliError::new(format!(
  219. "File `{}` already exists.\nMaybe you meant to specify another path?",
  220. config_path.display()
  221. )));
  222. }
  223. conf::create_config_file(&config_path)?;
  224. return Ok(());
  225. }
  226. #[cfg(feature = "cli-docs")]
  227. Some(SubCommand::Man(manopt)) => {
  228. let _page = manopt.page;
  229. const MANPAGES: [&'static str; 3] = [
  230. include_str!(concat!(env!("OUT_DIR"), "/meli.txt")),
  231. include_str!(concat!(env!("OUT_DIR"), "/meli.conf.txt")),
  232. include_str!(concat!(env!("OUT_DIR"), "/meli-themes.txt")),
  233. ];
  234. println!("{}", MANPAGES[_page as usize]);
  235. return Ok(());
  236. }
  237. #[cfg(not(feature = "cli-docs"))]
  238. Some(SubCommand::Man(_manopt)) => {
  239. return Err(MeliError::new("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
  240. }
  241. Some(SubCommand::PrintLoadedThemes) => {
  242. let s = conf::FileSettings::new()?;
  243. print!("{}", s.terminal.themes.to_string());
  244. return Ok(());
  245. }
  246. Some(SubCommand::PrintDefaultTheme) => {
  247. print!("{}", conf::Themes::default().key_to_string("dark", false));
  248. return Ok(());
  249. }
  250. Some(SubCommand::View { ref path }) => {
  251. if !path.exists() {
  252. return Err(MeliError::new(format!(
  253. "`{}` is not a valid path",
  254. path.display()
  255. )));
  256. } else if !path.is_file() {
  257. return Err(MeliError::new(format!(
  258. "`{}` is a directory",
  259. path.display()
  260. )));
  261. }
  262. }
  263. None => {}
  264. }
  265. /* Create a channel to communicate with other threads. The main process is the sole receiver.
  266. * */
  267. let (sender, receiver) = crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>());
  268. /* Catch SIGWINCH to handle terminal resizing */
  269. let signals = &[
  270. /* Catch SIGWINCH to handle terminal resizing */
  271. signal_hook::SIGWINCH,
  272. /* Catch SIGCHLD to handle embed applications status change */
  273. signal_hook::SIGCHLD,
  274. ];
  275. let signal_recvr = notify(signals, sender.clone())?;
  276. /* Create the application State. */
  277. let mut state;
  278. if let Some(SubCommand::View { path }) = opt.subcommand {
  279. let bytes = std::fs::read(&path)
  280. .chain_err_summary(|| format!("Could not read from `{}`", path.display()))?;
  281. let wrapper = EnvelopeWrapper::new(bytes)
  282. .chain_err_summary(|| format!("Could not parse `{}`", path.display()))?;
  283. state = State::new(
  284. Some(Settings::without_accounts().unwrap_or_default()),
  285. sender,
  286. receiver.clone(),
  287. )?;
  288. state.register_component(Box::new(EnvelopeView::new(wrapper, None, None, 0)));
  289. } else {
  290. state = State::new(None, sender, receiver.clone())?;
  291. #[cfg(feature = "svgscreenshot")]
  292. state.register_component(Box::new(components::svg::SVGScreenshotFilter::new()));
  293. let window = Box::new(Tabbed::new(
  294. vec![
  295. Box::new(listing::Listing::new(&mut state.context)),
  296. Box::new(ContactList::new(&state.context)),
  297. Box::new(StatusPanel::new(crate::conf::value(
  298. &state.context,
  299. "theme_default",
  300. ))),
  301. ],
  302. &state.context,
  303. ));
  304. let status_bar = Box::new(StatusBar::new(window));
  305. state.register_component(status_bar);
  306. let xdg_notifications = Box::new(components::notifications::XDGNotifications::new());
  307. state.register_component(xdg_notifications);
  308. state.register_component(Box::new(components::notifications::NotificationFilter {}));
  309. }
  310. let enter_command_mode: Key = state
  311. .context
  312. .settings
  313. .shortcuts
  314. .general
  315. .enter_command_mode
  316. .clone();
  317. /* Keep track of the input mode. See UIMode for details */
  318. 'main: loop {
  319. state.render();
  320. 'inner: loop {
  321. /* Check if any components have sent reply events to State. */
  322. let events: smallvec::SmallVec<[UIEvent; 8]> = state.context.replies();
  323. for e in events {
  324. state.rcv_event(e);
  325. }
  326. state.redraw();
  327. /* Poll on all channels. Currently we have the input channel for stdin, watching events and the signal watcher. */
  328. crossbeam::select! {
  329. recv(receiver) -> r => {
  330. match r {
  331. Ok(ThreadEvent::Pulse) | Ok(ThreadEvent::UIEvent(UIEvent::Timer(_))) => {},
  332. _ => {debug!(&r);}
  333. }
  334. match r.unwrap() {
  335. ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embed => {
  336. state.switch_to_main_screen();
  337. //_thread_handler.join().expect("Couldn't join on the associated thread");
  338. let self_pid = nix::unistd::Pid::this();
  339. nix::sys::signal::kill(self_pid, nix::sys::signal::Signal::SIGSTOP).unwrap();
  340. state.switch_to_alternate_screen();
  341. // BUG: thread sends input event after one received key
  342. state.update_size();
  343. state.render();
  344. state.redraw();
  345. },
  346. ThreadEvent::Input(raw_input @ (Key::Ctrl('l'), _)) => {
  347. /* Manual screen redraw */
  348. state.update_size();
  349. state.render();
  350. state.redraw();
  351. if state.mode == UIMode::Embed {
  352. state.rcv_event(UIEvent::EmbedInput(raw_input));
  353. state.redraw();
  354. }
  355. },
  356. ThreadEvent::Input((k, r)) => {
  357. match state.mode {
  358. UIMode::Normal => {
  359. match k {
  360. Key::Char('q') | Key::Char('Q') => {
  361. if state.can_quit_cleanly() {
  362. drop(state);
  363. break 'main;
  364. } else {
  365. state.redraw();
  366. }
  367. },
  368. _ if k == enter_command_mode => {
  369. state.mode = UIMode::Command;
  370. state.rcv_event(UIEvent::ChangeMode(UIMode::Command));
  371. state.redraw();
  372. }
  373. key => {
  374. state.rcv_event(UIEvent::Input(key));
  375. state.redraw();
  376. },
  377. }
  378. },
  379. UIMode::Insert => {
  380. match k {
  381. Key::Char('\n') | Key::Esc => {
  382. state.mode = UIMode::Normal;
  383. state.rcv_event(UIEvent::ChangeMode(UIMode::Normal));
  384. state.redraw();
  385. },
  386. k => {
  387. state.rcv_event(UIEvent::InsertInput(k));
  388. state.redraw();
  389. },
  390. }
  391. }
  392. UIMode::Command => {
  393. match k {
  394. Key::Char('\n') => {
  395. state.mode = UIMode::Normal;
  396. state.rcv_event(UIEvent::ChangeMode(UIMode::Normal));
  397. state.redraw();
  398. },
  399. k => {
  400. state.rcv_event(UIEvent::CmdInput(k));
  401. state.redraw();
  402. },
  403. }
  404. },
  405. UIMode::Embed => {
  406. state.rcv_event(UIEvent::EmbedInput((k,r)));
  407. state.redraw();
  408. },
  409. UIMode::Fork => {
  410. break 'inner; // `goto` 'reap loop, and wait on child.
  411. },
  412. }
  413. },
  414. ThreadEvent::RefreshMailbox(event) => {
  415. state.refresh_event(*event);
  416. state.redraw();
  417. },
  418. ThreadEvent::UIEvent(UIEvent::ChangeMode(f)) => {
  419. state.mode = f;
  420. if f == UIMode::Fork {
  421. break 'inner; // `goto` 'reap loop, and wait on child.
  422. }
  423. }
  424. ThreadEvent::UIEvent(e) => {
  425. state.rcv_event(e);
  426. state.redraw();
  427. },
  428. ThreadEvent::Pulse => {
  429. state.check_accounts();
  430. state.redraw();
  431. },
  432. ThreadEvent::NewThread(id, name) => {
  433. state.new_thread(id, name);
  434. },
  435. ThreadEvent::JobFinished(id) => {
  436. debug!("Job finished {}", id);
  437. for account in state.context.accounts.iter_mut() {
  438. if account.process_event(&id) {
  439. break;
  440. }
  441. }
  442. //state.new_thread(id, name);
  443. },
  444. }
  445. },
  446. recv(signal_recvr) -> sig => {
  447. match sig.unwrap() {
  448. signal_hook::SIGWINCH => {
  449. if state.mode != UIMode::Fork {
  450. state.update_size();
  451. state.render();
  452. state.redraw();
  453. }
  454. },
  455. signal_hook::SIGCHLD => {
  456. state.rcv_event(UIEvent::EmbedInput((Key::Null, vec![0])));
  457. state.redraw();
  458. }
  459. other => {
  460. debug!("got other signal: {:?}", other);
  461. }
  462. }
  463. },
  464. }
  465. } // end of 'inner
  466. 'reap: loop {
  467. match state.try_wait_on_child() {
  468. Some(true) => {
  469. state.restore_input();
  470. state.switch_to_alternate_screen();
  471. }
  472. Some(false) => {
  473. use std::{thread, time};
  474. let ten_millis = time::Duration::from_millis(1500);
  475. thread::sleep(ten_millis);
  476. continue 'reap;
  477. }
  478. None => {
  479. state.mode = UIMode::Normal;
  480. state.render();
  481. break 'reap;
  482. }
  483. }
  484. }
  485. }
  486. Ok(())
  487. }