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.

424 lines
17KB

  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 the `ui` module.
  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::io::Write;
  30. use std::path::{Path, PathBuf};
  31. #[global_allocator]
  32. static GLOBAL: System = System;
  33. use ui;
  34. pub use melib::*;
  35. pub use ui::*;
  36. use nix;
  37. use std::os::raw::c_int;
  38. use xdg;
  39. fn notify(
  40. signals: &[c_int],
  41. sender: crossbeam::channel::Sender<ThreadEvent>,
  42. ) -> std::result::Result<crossbeam::channel::Receiver<c_int>, std::io::Error> {
  43. let (s, r) = crossbeam::channel::bounded(100);
  44. let signals = signal_hook::iterator::Signals::new(signals)?;
  45. std::thread::spawn(move || {
  46. let mut ctr = 0;
  47. loop {
  48. ctr %= 3;
  49. if ctr == 0 {
  50. sender.send(ThreadEvent::Pulse).unwrap();
  51. }
  52. for signal in signals.pending() {
  53. s.send(signal).unwrap();
  54. }
  55. std::thread::sleep(std::time::Duration::from_millis(100));
  56. ctr += 1;
  57. }
  58. });
  59. Ok(r)
  60. }
  61. macro_rules! error_and_exit {
  62. ($($err:expr),*) => {{
  63. return Err(MeliError::new(format!($($err),*)));
  64. }}
  65. }
  66. #[derive(Debug)]
  67. struct CommandLineArguments {
  68. create_config: Option<String>,
  69. test_config: Option<String>,
  70. config: Option<String>,
  71. help: bool,
  72. version: bool,
  73. }
  74. fn main() {
  75. ::std::process::exit(match run_app() {
  76. Ok(()) => 0,
  77. Err(err) => {
  78. eprintln!("{}", err);
  79. 1
  80. }
  81. });
  82. }
  83. fn run_app() -> Result<()> {
  84. enum CommandLineFlags {
  85. CreateConfig,
  86. TestConfig,
  87. Config,
  88. }
  89. use CommandLineFlags::*;
  90. let mut prev: Option<CommandLineFlags> = None;
  91. let mut args = CommandLineArguments {
  92. create_config: None,
  93. test_config: None,
  94. config: None,
  95. help: false,
  96. version: false,
  97. };
  98. for i in std::env::args().skip(1) {
  99. match i.as_str() {
  100. "--test-config" => match prev {
  101. None => prev = Some(TestConfig),
  102. Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
  103. Some(Config) => error_and_exit!("invalid value for flag `--config`"),
  104. Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
  105. },
  106. "--create-config" => match prev {
  107. None => prev = Some(CreateConfig),
  108. Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
  109. Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
  110. Some(Config) => error_and_exit!("invalid value for flag `--config`"),
  111. },
  112. "--config" | "-c" => match prev {
  113. None => prev = Some(Config),
  114. Some(CreateConfig) if args.create_config.is_none() => {
  115. args.config = Some(String::new());
  116. prev = Some(Config);
  117. }
  118. Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
  119. Some(Config) => error_and_exit!("invalid value for flag `--config`"),
  120. Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
  121. },
  122. "--help" | "-h" => {
  123. args.help = true;
  124. }
  125. "--version" | "-v" => {
  126. args.version = true;
  127. }
  128. e => match prev {
  129. None => error_and_exit!("error: value without command {}", e),
  130. Some(CreateConfig) if args.create_config.is_none() => {
  131. args.create_config = Some(i);
  132. prev = None;
  133. }
  134. Some(Config) if args.config.is_none() => {
  135. args.config = Some(i);
  136. prev = None;
  137. }
  138. Some(TestConfig) if args.test_config.is_none() => {
  139. args.test_config = Some(i);
  140. prev = None;
  141. }
  142. Some(TestConfig) => error_and_exit!("Duplicate value for flag `--test-config`"),
  143. Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"),
  144. Some(Config) => error_and_exit!("Duplicate value for flag `--config`"),
  145. },
  146. }
  147. }
  148. if args.help {
  149. println!("usage:\tmeli [--create-config[ PATH]] [--config[ PATH]|-c[ PATH]]");
  150. println!("\tmeli --help");
  151. println!("\tmeli --version");
  152. println!("");
  153. println!("\t--help, -h\t\tshow this message and exit");
  154. println!("\t--version, -v\t\tprint version and exit");
  155. println!("\t--create-config[ PATH]\tCreate 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");
  156. println!(
  157. "\t--test-config PATH\tTest a configuration file for syntax issues or missing options."
  158. );
  159. println!("\t--config PATH, -c PATH\tUse specified configuration file");
  160. return Ok(());
  161. }
  162. if args.version {
  163. println!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0"));
  164. return Ok(());
  165. }
  166. match prev {
  167. None => {}
  168. Some(CreateConfig) if args.create_config.is_none() => args.create_config = Some("".into()),
  169. Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"),
  170. Some(Config) => error_and_exit!("error: flag without value: `--config`"),
  171. Some(TestConfig) => error_and_exit!("error: flag without value: `--test-config`"),
  172. };
  173. if let Some(config_path) = args.test_config.as_ref() {
  174. ui::conf::FileSettings::validate(config_path)?;
  175. return Ok(());
  176. }
  177. if let Some(config_path) = args.create_config.as_mut() {
  178. let config_path: PathBuf = if config_path.is_empty() {
  179. let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").unwrap();
  180. xdg_dirs.place_config_file("config").map_err(|e| {
  181. MeliError::new(format!(
  182. "Cannot create configuration directory in {}:\n{}",
  183. xdg_dirs.get_config_home().display(),
  184. e
  185. ))
  186. })?
  187. } else {
  188. Path::new(config_path).to_path_buf()
  189. };
  190. if config_path.exists() {
  191. return Err(MeliError::new(format!("File `{}` already exists.\nMaybe you meant to specify another path with --create-config=PATH", config_path.display())));
  192. }
  193. let mut file = std::fs::OpenOptions::new()
  194. .write(true)
  195. .create_new(true)
  196. .open(config_path.as_path())
  197. .map_err(|e| MeliError::new(format!("Could not create config file:\n{}", e)))?;
  198. file.write_all(include_bytes!("../sample-config"))
  199. .map_err(|e| MeliError::new(format!("Could not write to config file:\n{}", e)))?;
  200. println!("Written example configuration to {}", config_path.display());
  201. return Ok(());
  202. }
  203. if let Some(config_location) = args.config.as_ref() {
  204. std::env::set_var("MELI_CONFIG", config_location);
  205. }
  206. /* Create the application State. */
  207. let mut state = State::new()?;
  208. let receiver = state.receiver();
  209. let sender = state.sender();
  210. /* Catch SIGWINCH to handle terminal resizing */
  211. let signals = &[
  212. /* Catch SIGWINCH to handle terminal resizing */
  213. signal_hook::SIGWINCH,
  214. /* Catch SIGCHLD to handle embed applications status change */
  215. signal_hook::SIGCHLD,
  216. ];
  217. let signal_recvr = notify(signals, sender)?;
  218. let window = Box::new(Tabbed::new(vec![
  219. Box::new(listing::Listing::new(&state.context.accounts)),
  220. Box::new(ContactList::new(&state.context)),
  221. Box::new(StatusPanel::new()),
  222. ]));
  223. let status_bar = Box::new(StatusBar::new(window));
  224. state.register_component(status_bar);
  225. let xdg_notifications = Box::new(ui::components::notifications::XDGNotifications {});
  226. state.register_component(xdg_notifications);
  227. state.register_component(Box::new(
  228. ui::components::notifications::NotificationFilter {},
  229. ));
  230. /* Keep track of the input mode. See ui::UIMode for details */
  231. 'main: loop {
  232. state.render();
  233. 'inner: loop {
  234. /* Check if any components have sent reply events to State. */
  235. let events: Vec<UIEvent> = state.context.replies();
  236. for e in events {
  237. state.rcv_event(e);
  238. }
  239. state.redraw();
  240. /* Poll on all channels. Currently we have the input channel for stdin, watching events and the signal watcher. */
  241. crossbeam::select! {
  242. recv(receiver) -> r => {
  243. match r {
  244. Ok(ThreadEvent::Pulse) => {},
  245. _ => {debug!(&r);}
  246. }
  247. match r.unwrap() {
  248. ThreadEvent::Input(Key::Ctrl('z')) => {
  249. state.switch_to_main_screen();
  250. //_thread_handler.join().expect("Couldn't join on the associated thread");
  251. let self_pid = nix::unistd::Pid::this();
  252. nix::sys::signal::kill(self_pid, nix::sys::signal::Signal::SIGSTOP).unwrap();
  253. state.switch_to_alternate_screen();
  254. state.restore_input();
  255. // BUG: thread sends input event after one received key
  256. state.update_size();
  257. state.render();
  258. state.redraw();
  259. },
  260. ThreadEvent::Input(Key::Ctrl('l')) => {
  261. /* Manual screen redraw */
  262. state.update_size();
  263. state.render();
  264. state.redraw();
  265. },
  266. ThreadEvent::InputRaw(raw_input @ (Key::Ctrl('l'), _)) => {
  267. /* Manual screen redraw */
  268. state.update_size();
  269. state.render();
  270. state.redraw();
  271. state.rcv_event(UIEvent::EmbedInput(raw_input));
  272. state.redraw();
  273. },
  274. ThreadEvent::Input(k) => {
  275. match state.mode {
  276. UIMode::Normal => {
  277. match k {
  278. Key::Char('q') | Key::Char('Q') => {
  279. if state.can_quit_cleanly() {
  280. drop(state);
  281. break 'main;
  282. } else {
  283. state.redraw();
  284. }
  285. },
  286. Key::Char(' ') => {
  287. state.mode = UIMode::Execute;
  288. state.rcv_event(UIEvent::ChangeMode(UIMode::Execute));
  289. state.redraw();
  290. }
  291. key => {
  292. state.rcv_event(UIEvent::Input(key));
  293. state.redraw();
  294. },
  295. }
  296. },
  297. UIMode::Insert => {
  298. match k {
  299. Key::Char('\n') | Key::Esc => {
  300. state.mode = UIMode::Normal;
  301. state.rcv_event(UIEvent::ChangeMode(UIMode::Normal));
  302. state.redraw();
  303. },
  304. k => {
  305. state.rcv_event(UIEvent::InsertInput(k));
  306. state.redraw();
  307. },
  308. }
  309. }
  310. UIMode::Execute => {
  311. match k {
  312. Key::Char('\n') | Key::Esc => {
  313. state.mode = UIMode::Normal;
  314. state.rcv_event(UIEvent::ChangeMode(UIMode::Normal));
  315. state.redraw();
  316. },
  317. k => {
  318. state.rcv_event(UIEvent::ExInput(k));
  319. state.redraw();
  320. },
  321. }
  322. },
  323. UIMode::Embed => state.redraw(),
  324. UIMode::Fork => {
  325. break 'inner; // `goto` 'reap loop, and wait on child.
  326. },
  327. }
  328. },
  329. ThreadEvent::InputRaw(raw_input) => {
  330. state.rcv_event(UIEvent::EmbedInput(raw_input));
  331. state.redraw();
  332. },
  333. ThreadEvent::RefreshMailbox(event) => {
  334. state.refresh_event(*event);
  335. state.redraw();
  336. },
  337. ThreadEvent::UIEvent(UIEvent::ChangeMode(f)) => {
  338. state.mode = f;
  339. if f == UIMode::Fork {
  340. break 'inner; // `goto` 'reap loop, and wait on child.
  341. }
  342. }
  343. ThreadEvent::UIEvent(e) => {
  344. state.rcv_event(e);
  345. state.render();
  346. },
  347. ThreadEvent::Pulse => {
  348. state.redraw();
  349. },
  350. ThreadEvent::ThreadJoin(id) => {
  351. state.join(id);
  352. },
  353. }
  354. },
  355. recv(signal_recvr) -> sig => {
  356. match sig.unwrap() {
  357. signal_hook::SIGWINCH => {
  358. if state.mode != UIMode::Fork {
  359. state.update_size();
  360. state.render();
  361. state.redraw();
  362. }
  363. },
  364. _ => {}
  365. }
  366. },
  367. }
  368. } // end of 'inner
  369. 'reap: loop {
  370. match state.try_wait_on_child() {
  371. Some(true) => {
  372. state.restore_input();
  373. state.switch_to_alternate_screen();
  374. }
  375. Some(false) => {
  376. use std::{thread, time};
  377. let ten_millis = time::Duration::from_millis(1500);
  378. thread::sleep(ten_millis);
  379. continue 'reap;
  380. }
  381. None => {
  382. state.mode = UIMode::Normal;
  383. state.render();
  384. break 'reap;
  385. }
  386. }
  387. }
  388. }
  389. Ok(())
  390. }