main.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. // TODO: CLI args for save location and server address
  2. mod actions;
  3. mod message;
  4. mod persistence;
  5. mod ui;
  6. use cursive::views::{
  7. Button, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ScrollView, SelectView,
  8. TextArea, TextView,
  9. };
  10. use cursive::{event, Cursive};
  11. use cursive::{traits::*, CursiveExt as _};
  12. use message::Message;
  13. use std::collections::HashMap;
  14. use std::io::{Read as _, Write as _};
  15. use std::thread;
  16. use utils::serialize::Serialize;
  17. use ui::{
  18. DIALOGUE_MIN_SIZE, INPUT_BUTTON_ID, INPUT_CLEAR_BUTTON_ID, INPUT_FIELD_ID, INPUT_PANEL_ID,
  19. LANGUAGE_BUTTON_ID, MESSAGE_PANEL_ID, REFRESH_BUTTON_ID, USERNAME_BUTTON_ID, USERNAME_FIELD_ID,
  20. USERS_PANEL_ID, USER_PANEL_SIZE,
  21. };
  22. const MAX_MESSAGE_LENGTH: usize = 512;
  23. const MAX_USERNAME_LENGTH: usize = 16;
  24. const DEFAULT_USERNAME_PREFIX: &str = "Myst";
  25. const SAVE_FILE: &str = "savedata.bin";
  26. const REMOTE_REFRESH_RATE: u64 = 10;
  27. // TODO: Add server refresh rate
  28. #[derive(Clone)]
  29. pub struct Appdata {
  30. pub username: String,
  31. pub language: ui::Language,
  32. pub blocked_phrases: Vec<String>,
  33. pub blocked_phrases_censor_char: char,
  34. pub messages: HashMap<String, Message>,
  35. pub quick_close_window_count: usize,
  36. pub api_endpoint: String,
  37. }
  38. impl Default for Appdata {
  39. fn default() -> Self {
  40. Self::new()
  41. }
  42. }
  43. impl Appdata {
  44. pub fn new() -> Self {
  45. Self {
  46. messages: HashMap::new(),
  47. username: format!(
  48. "{}#{}",
  49. DEFAULT_USERNAME_PREFIX,
  50. utils::rng::random_numeric_string(4)
  51. ),
  52. language: ui::Language::English,
  53. quick_close_window_count: 0,
  54. api_endpoint: "http://127.0.0.1:8080".to_string(),
  55. blocked_phrases: Vec::new(),
  56. blocked_phrases_censor_char: '*',
  57. }
  58. }
  59. }
  60. pub fn get_appdata(siv: &mut Cursive) -> Appdata {
  61. siv.with_user_data(|appdata: &mut Appdata| appdata.clone())
  62. .expect("Failed to retrieve appdata.")
  63. }
  64. pub fn save_appdata(siv: &mut Cursive) {
  65. let appdata = get_appdata(siv);
  66. let savedata = persistence::Savedata::from(appdata);
  67. let bytes = savedata.serialize_checked();
  68. let mut file = std::fs::File::create(SAVE_FILE).expect("Failed to create savedata file.");
  69. file.write_all(&bytes)
  70. .expect("Failed to write savedata file.");
  71. }
  72. pub fn load_appdata(siv: &mut Cursive) -> std::io::Result<()> {
  73. let mut file = std::fs::File::open(SAVE_FILE)?;
  74. let mut bytes = Vec::new();
  75. file.read_to_end(&mut bytes)?;
  76. let savedata = persistence::Savedata::deserialize_checked(bytes).unwrap();
  77. let appdata = Appdata::from(savedata);
  78. siv.set_user_data(appdata);
  79. Ok(())
  80. }
  81. fn main() {
  82. utils::rng::shuffle_rng();
  83. let mut siv = Cursive::default();
  84. siv.set_user_data(Appdata::new());
  85. load_appdata(&mut siv);
  86. // Global hotkeys
  87. siv.add_global_callback(event::Key::Backspace, |siv| {
  88. let _ = siv.focus_name(INPUT_FIELD_ID);
  89. });
  90. // Background thread
  91. {
  92. let cb_sink = siv.cb_sink().clone();
  93. let mut timer = 0;
  94. thread::spawn(move || loop {
  95. cb_sink
  96. .send(Box::new(move |siv| {
  97. ui::visual_update(siv);
  98. if timer % REMOTE_REFRESH_RATE == 0 {
  99. actions::load_messages(siv);
  100. }
  101. save_appdata(siv);
  102. }))
  103. .expect("Failed to send callback from background thread.");
  104. timer += 1;
  105. thread::sleep(std::time::Duration::from_secs(1));
  106. });
  107. }
  108. // Users
  109. let users_panel = {
  110. let mut users_view: SelectView<String> = SelectView::new();
  111. users_view.set_on_submit(move |siv, user: &String| actions::on_user_click(siv, user));
  112. Panel::new(ScrollView::new(users_view)).with_name(USERS_PANEL_ID)
  113. };
  114. // Messages
  115. let messages_panel = {
  116. let mut view: SelectView<String> = SelectView::new();
  117. view.set_on_submit(move |siv, message_id: &str| actions::on_message_click(siv, message_id));
  118. Panel::new(ScrollView::new(view)).with_name(MESSAGE_PANEL_ID)
  119. };
  120. // Input
  121. let input_panel = {
  122. let input_field = TextArea::new()
  123. .with_name(INPUT_FIELD_ID)
  124. .full_width()
  125. .max_height(10); // NOTE: 10 seems to be the size of 2 vertical buttons
  126. let input_button =
  127. Button::new("", move |siv| actions::on_input_submit(siv, INPUT_FIELD_ID))
  128. .with_name(INPUT_BUTTON_ID);
  129. let input_clear_button =
  130. Button::new("", actions::clear_input_field).with_name(INPUT_CLEAR_BUTTON_ID);
  131. Panel::new(
  132. LinearLayout::horizontal().child(input_field).child(
  133. LinearLayout::vertical()
  134. .child(input_button)
  135. .child(input_clear_button),
  136. ),
  137. )
  138. .with_name(INPUT_PANEL_ID)
  139. };
  140. // Language selector
  141. let language_button = Button::new("", move |siv| {
  142. // NOTE: This must match the order in the SelectView
  143. let current_language_index = match get_appdata(siv).language {
  144. ui::Language::English => 0,
  145. ui::Language::Dutch => 1,
  146. ui::Language::Japanese => 2,
  147. };
  148. let language = get_appdata(siv).language;
  149. ui::keybind_close_once(siv);
  150. siv.add_layer(
  151. cursive::views::Dialog::new()
  152. .content(
  153. LinearLayout::vertical().child(
  154. SelectView::<ui::Language>::new()
  155. .item("English", ui::Language::English)
  156. .item("Nederlands", ui::Language::Dutch)
  157. .item("日本語", ui::Language::Japanese)
  158. .selected(current_language_index)
  159. .on_submit(move |siv, language| {
  160. ui::change_language(siv, *language);
  161. ui::keybind_close_manual_end(siv, false);
  162. }),
  163. ),
  164. )
  165. .title(ui::Labels::SetLanguage.localize(language))
  166. .button(ui::Labels::Cancel.localize(language), |siv| {
  167. ui::keybind_close_manual_end(siv, false);
  168. })
  169. .min_size(DIALOGUE_MIN_SIZE),
  170. );
  171. })
  172. .with_name(LANGUAGE_BUTTON_ID);
  173. // Current username panel
  174. let current_username_panel = {
  175. let current_username = get_appdata(&mut siv).username.clone();
  176. let view = TextView::new(current_username);
  177. Panel::new(view).with_name(ui::CURRENT_USERNAME_PANEL_ID)
  178. };
  179. // Set username
  180. let username_button = Button::new("", move |siv| {
  181. let current_name = get_appdata(siv).username.clone();
  182. let language = get_appdata(siv).language;
  183. ui::keybind_close_once(siv);
  184. siv.add_layer(
  185. cursive::views::Dialog::new()
  186. .title(ui::Labels::Username.localize(language))
  187. .content(
  188. LinearLayout::vertical().child(
  189. EditView::new()
  190. .content(current_name)
  191. .with_name(USERNAME_FIELD_ID)
  192. .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
  193. ),
  194. )
  195. .button(ui::Labels::Ok.localize(language), |siv| {
  196. let input = siv
  197. .call_on_name(USERNAME_FIELD_ID, |view: &mut EditView| view.get_content())
  198. .expect("Failed to retrieve username input.");
  199. if message::is_valid_username(&input) {
  200. // Valid
  201. siv.with_user_data(|appdata: &mut Appdata| {
  202. appdata.username = input.to_string();
  203. });
  204. ui::visual_update(siv);
  205. ui::keybind_close_manual_end(siv, false);
  206. } else {
  207. // Invalid
  208. let language = get_appdata(siv).language;
  209. ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
  210. ui::error(
  211. siv,
  212. ui::Labels::InvalidUsernameExplination.localize(language),
  213. );
  214. }
  215. })
  216. .button(ui::Labels::Cancel.localize(language), |siv| {
  217. ui::keybind_close_manual_end(siv, false);
  218. })
  219. .min_size(DIALOGUE_MIN_SIZE),
  220. );
  221. })
  222. .with_name(USERNAME_BUTTON_ID);
  223. // Refresh button
  224. let refresh_button = Button::new("", move |siv| {
  225. if actions::load_messages(siv).is_err() {
  226. let language = get_appdata(siv).language;
  227. ui::error(siv, ui::Labels::FailedToLoadMessages.localize(language));
  228. }
  229. ui::visual_update(siv);
  230. })
  231. .with_name(REFRESH_BUTTON_ID);
  232. // Blocked words list
  233. let blocked_words_list_button = {
  234. Button::new("", move |siv| {
  235. let appdata = get_appdata(siv);
  236. let language = appdata.language;
  237. let wordslist_id = utils::new_id();
  238. let wordslist_id_clone = wordslist_id.clone();
  239. let wordslist_id_typingarea_clone = wordslist_id.clone();
  240. let mut wordslist_view =
  241. SelectView::<String>::new().on_submit(move |siv, word: &str| {
  242. siv.with_user_data(|appdata: &mut Appdata| {
  243. appdata.blocked_phrases.retain(|x| x != word);
  244. });
  245. siv.call_on_name(&wordslist_id_clone, |view: &mut NamedView<SelectView>| {
  246. let idx = view
  247. .get_mut()
  248. .selected_id()
  249. .expect("Selection should exist when fired from submit.");
  250. view.get_mut().remove_item(idx);
  251. });
  252. ui::visual_update(siv);
  253. });
  254. wordslist_view.add_all_str(&appdata.blocked_phrases);
  255. let typingarea_id = utils::new_id();
  256. let appdata_typingarea_clone = appdata.clone();
  257. let typing_area = EditView::new().with_name(&typingarea_id);
  258. let typing_button = Button::new(ui::Labels::Submit.localize(language), move |siv| {
  259. let text = siv
  260. .call_on_name(&typingarea_id, |view: &mut EditView| {
  261. let s = view.get_content();
  262. view.set_content("");
  263. s
  264. })
  265. .map(|x| x.to_string())
  266. .unwrap_or("".to_string());
  267. if appdata_typingarea_clone.blocked_phrases.contains(&text) || text.is_empty() {
  268. return;
  269. }
  270. siv.with_user_data(|appdata: &mut Appdata| {
  271. appdata.blocked_phrases.push(text.clone());
  272. });
  273. siv.call_on_name(
  274. &wordslist_id_typingarea_clone,
  275. |view: &mut NamedView<SelectView>| {
  276. view.get_mut().add_item_str(text);
  277. },
  278. );
  279. ui::visual_update(siv);
  280. });
  281. let d = Dialog::new()
  282. .title(ui::Labels::BlockedWords.localize(language))
  283. .button(ui::Labels::Close.localize(language), |siv| {
  284. ui::keybind_close_manual_end(siv, false);
  285. })
  286. .content(
  287. LinearLayout::vertical()
  288. .child(ScrollView::new(wordslist_view.with_name(wordslist_id)))
  289. .child(DummyView.fixed_height(1))
  290. .child(
  291. LinearLayout::horizontal()
  292. .child(typing_area.min_width(DIALOGUE_MIN_SIZE.0 as usize))
  293. .child(typing_button),
  294. ),
  295. );
  296. ui::keybind_close_once(siv);
  297. siv.add_layer(d);
  298. })
  299. .with_name(ui::BLOCKED_WORDS_BUTTON_ID)
  300. };
  301. // Main layout
  302. let main_layout = LinearLayout::vertical()
  303. .child(
  304. LinearLayout::horizontal()
  305. .child(
  306. LinearLayout::vertical()
  307. .child(users_panel.resized(
  308. cursive::view::SizeConstraint::Fixed(USER_PANEL_SIZE),
  309. cursive::view::SizeConstraint::Full,
  310. ))
  311. .child(current_username_panel),
  312. )
  313. .child(messages_panel.full_width()),
  314. )
  315. .child(input_panel)
  316. .child(
  317. LinearLayout::horizontal()
  318. .child(language_button)
  319. .child(DummyView.full_width())
  320. .child(username_button)
  321. .child(DummyView.full_width())
  322. .child(blocked_words_list_button)
  323. .child(DummyView.full_width())
  324. .child(refresh_button)
  325. .full_width(),
  326. );
  327. siv.add_fullscreen_layer(main_layout);
  328. siv.run();
  329. }