main.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. mod actions;
  2. mod message;
  3. mod persistence;
  4. mod ui;
  5. use cursive::reexports::log;
  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 DEFAULT_CHANNEL: &str = "Root";
  26. const SAVE_FILE: &str = "savedata.bin";
  27. const SAVE_FILE_FUZZY: u64 = 0b0110110001101001011001110110110101100001001000000101100001000100;
  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 api_endpoint: String,
  35. pub api_refresh_rate: usize,
  36. pub api_password: String,
  37. pub current_channel: String,
  38. pub messages: HashMap<String, Message>,
  39. pub quick_close_window_count: usize,
  40. pub local_channels: Vec<String>,
  41. }
  42. impl Default for Appdata {
  43. fn default() -> Self {
  44. Self::new()
  45. }
  46. }
  47. impl Appdata {
  48. pub fn new() -> Self {
  49. Self {
  50. messages: HashMap::new(),
  51. username: format!(
  52. "{}#{}",
  53. DEFAULT_USERNAME_PREFIX,
  54. utils::rng::random_numeric_string(4)
  55. ),
  56. language: ui::Language::English,
  57. quick_close_window_count: 0,
  58. api_endpoint: String::new(),
  59. api_refresh_rate: 10,
  60. blocked_phrases: Vec::new(),
  61. blocked_phrases_censor_char: '*',
  62. current_channel: DEFAULT_CHANNEL.to_string(),
  63. local_channels: vec![DEFAULT_CHANNEL.to_string()],
  64. api_password: String::new(),
  65. }
  66. }
  67. }
  68. pub fn get_appdata(siv: &mut Cursive) -> Appdata {
  69. siv.with_user_data(|appdata: &mut Appdata| appdata.clone())
  70. .expect("Failed to retrieve appdata.")
  71. }
  72. pub fn save_appdata(siv: &mut Cursive) {
  73. let appdata = get_appdata(siv);
  74. let savedata = persistence::Savedata::from(appdata);
  75. let bytes = savedata.serialize_checked();
  76. let fuzzy_bytes = utils::binary::fuzzy_bytes(bytes, SAVE_FILE_FUZZY);
  77. let mut file = std::fs::File::create(SAVE_FILE).expect("Failed to create savedata file.");
  78. file.write_all(&fuzzy_bytes)
  79. .expect("Failed to write savedata file.");
  80. log::info!("Wrote savadata to file.");
  81. }
  82. pub fn load_appdata(siv: &mut Cursive) -> std::io::Result<()> {
  83. let mut df = logging::warn_deferred("Savedata file not found; using defaults");
  84. let mut file = std::fs::File::open(SAVE_FILE)?;
  85. let mut bytes = Vec::new();
  86. file.read_to_end(&mut bytes)?;
  87. let useful_bytes = utils::binary::fuzzy_bytes(bytes, SAVE_FILE_FUZZY);
  88. df.cancel();
  89. let Ok(savedata) = persistence::Savedata::deserialize_checked(useful_bytes) else {
  90. // If the file is corrupted, create a new one
  91. logging::warn("Savedata file corrupted; using defaults.");
  92. return Ok(());
  93. };
  94. let appdata = Appdata::from(savedata);
  95. siv.set_user_data(appdata);
  96. logging::info("Savedata file loaded.");
  97. Ok(())
  98. }
  99. fn main() {
  100. if cfg!(debug_assertions) {
  101. logging::set_global_log_level(logging::LogLevel::Trace);
  102. } else {
  103. logging::set_global_log_level(logging::LogLevel::Info);
  104. }
  105. utils::rng::shuffle_rng();
  106. let mut siv = Cursive::default();
  107. siv.set_user_data(Appdata::new());
  108. // TODO (low): Add a notice when the file is corrupted.
  109. let _ = load_appdata(&mut siv);
  110. // Global hotkeys
  111. siv.add_global_callback(event::Key::Backspace, |siv| {
  112. let _ = siv.focus_name(INPUT_FIELD_ID);
  113. });
  114. // Background thread
  115. {
  116. let cb_sink = siv.cb_sink().clone();
  117. let mut timer = 0;
  118. thread::spawn(move || loop {
  119. cb_sink
  120. .send(Box::new(move |siv| {
  121. let appdata = get_appdata(siv);
  122. ui::visual_update(siv);
  123. if timer % appdata.api_refresh_rate == 0 {
  124. // TODO (low): Add a notice when automatic refresh fails.
  125. let _ = actions::load_messages(siv);
  126. }
  127. save_appdata(siv);
  128. }))
  129. .expect("Failed to send callback from background thread.");
  130. timer += 1;
  131. thread::sleep(std::time::Duration::from_secs(1));
  132. });
  133. }
  134. // Channels
  135. let channels_panel = {
  136. let mut view: SelectView<String> = SelectView::new();
  137. view.set_on_submit(move |siv, channel_id: &str| {
  138. siv.with_user_data(|appdata: &mut Appdata| {
  139. appdata.current_channel = channel_id.to_string();
  140. });
  141. ui::visual_update(siv);
  142. });
  143. Panel::new(ScrollView::new(view)).with_name(ui::CHANNEL_VIEW_ID)
  144. };
  145. let channel_current_panel = {
  146. let view = TextView::new("");
  147. Panel::new(view).with_name(ui::CHANNEL_CURRENT_PANEL_ID)
  148. };
  149. let channels_new_button = Button::new("", move |siv| {
  150. ui::keybind_setup_close_once(siv);
  151. let language = get_appdata(siv).language;
  152. siv.add_layer(
  153. cursive::views::Dialog::new()
  154. .title(ui::Labels::NewChannel.localize(language))
  155. .content(
  156. LinearLayout::vertical().child(
  157. EditView::new()
  158. .with_name(ui::CHANNEL_NEW_FIELD_ID)
  159. .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
  160. ),
  161. )
  162. .button(ui::Labels::Ok.localize(language), move |siv| {
  163. let input = siv
  164. .call_on_name(ui::CHANNEL_NEW_FIELD_ID, |view: &mut EditView| {
  165. view.get_content()
  166. })
  167. .expect("Failed to retrieve channel creation input.");
  168. if message::is_valid_username(&input) {
  169. // Valid
  170. siv.with_user_data(|appdata: &mut Appdata| {
  171. appdata.local_channels.push(input.to_string());
  172. });
  173. save_appdata(siv);
  174. ui::visual_update(siv);
  175. ui::keybind_close_manual_end(siv, false);
  176. } else {
  177. // Invalid
  178. ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
  179. ui::error(
  180. siv,
  181. ui::Labels::InvalidChannelNameExplination.localize(language),
  182. );
  183. }
  184. })
  185. .button(ui::Labels::Cancel.localize(language), |siv| {
  186. ui::keybind_close_manual_end(siv, false);
  187. })
  188. .min_size(DIALOGUE_MIN_SIZE),
  189. );
  190. })
  191. .with_name(ui::CHANNEL_NEW_BUTTON_ID);
  192. // Users
  193. let users_panel = {
  194. let mut users_view: SelectView<String> = SelectView::new();
  195. users_view.set_on_submit(move |siv, user: &String| actions::on_user_click(siv, user));
  196. Panel::new(ScrollView::new(users_view)).with_name(USERS_PANEL_ID)
  197. };
  198. // Messages
  199. let messages_panel = {
  200. let mut view: SelectView<String> = SelectView::new();
  201. view.set_on_submit(move |siv, message_id: &str| actions::on_message_click(siv, message_id));
  202. Panel::new(ScrollView::new(view)).with_name(MESSAGE_PANEL_ID)
  203. };
  204. // Input
  205. let input_panel = {
  206. let input_field = TextArea::new()
  207. .with_name(INPUT_FIELD_ID)
  208. .full_width()
  209. .max_height(10); // NOTE: 10 seems to be the size of 2 vertical buttons
  210. let input_button =
  211. Button::new("", move |siv| actions::on_input_submit(siv, INPUT_FIELD_ID))
  212. .with_name(INPUT_BUTTON_ID);
  213. let input_clear_button =
  214. Button::new("", actions::clear_input_field).with_name(INPUT_CLEAR_BUTTON_ID);
  215. Panel::new(
  216. LinearLayout::horizontal().child(input_field).child(
  217. LinearLayout::vertical()
  218. .child(input_button)
  219. .child(input_clear_button),
  220. ),
  221. )
  222. .with_name(INPUT_PANEL_ID)
  223. };
  224. // Language selector
  225. let language_button = Button::new("", move |siv| {
  226. // NOTE: This must match the order in the SelectView
  227. let current_language_index = match get_appdata(siv).language {
  228. ui::Language::English => 0,
  229. ui::Language::Dutch => 1,
  230. ui::Language::Japanese => 2,
  231. };
  232. let language = get_appdata(siv).language;
  233. ui::keybind_setup_close_once(siv);
  234. siv.add_layer(
  235. cursive::views::Dialog::new()
  236. .content(
  237. LinearLayout::vertical().child(
  238. SelectView::<ui::Language>::new()
  239. .item("English", ui::Language::English)
  240. .item("Nederlands", ui::Language::Dutch)
  241. .item("日本語", ui::Language::Japanese)
  242. .selected(current_language_index)
  243. .on_submit(move |siv, language| {
  244. ui::change_language(siv, *language);
  245. ui::keybind_close_manual_end(siv, false);
  246. }),
  247. ),
  248. )
  249. .title(ui::Labels::SetLanguage.localize(language))
  250. .button(ui::Labels::Cancel.localize(language), |siv| {
  251. ui::keybind_close_manual_end(siv, false);
  252. })
  253. .min_size(DIALOGUE_MIN_SIZE),
  254. );
  255. })
  256. .with_name(LANGUAGE_BUTTON_ID);
  257. // Current username panel
  258. let current_username_panel = {
  259. let current_username = get_appdata(&mut siv).username.clone();
  260. let view = TextView::new(current_username);
  261. Panel::new(view).with_name(ui::CURRENT_USERNAME_PANEL_ID)
  262. };
  263. // Set username
  264. let username_button = Button::new("", move |siv| {
  265. let current_name = get_appdata(siv).username.clone();
  266. let language = get_appdata(siv).language;
  267. ui::keybind_setup_close_once(siv);
  268. siv.add_layer(
  269. cursive::views::Dialog::new()
  270. .title(ui::Labels::Username.localize(language))
  271. .content(
  272. LinearLayout::vertical().child(
  273. EditView::new()
  274. .content(current_name)
  275. .with_name(USERNAME_FIELD_ID)
  276. .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
  277. ),
  278. )
  279. .button(ui::Labels::Ok.localize(language), |siv| {
  280. let input = siv
  281. .call_on_name(USERNAME_FIELD_ID, |view: &mut EditView| view.get_content())
  282. .expect("Failed to retrieve username input.");
  283. if message::is_valid_username(&input) {
  284. // Valid
  285. siv.with_user_data(|appdata: &mut Appdata| {
  286. appdata.username = input.to_string();
  287. });
  288. save_appdata(siv);
  289. ui::visual_update(siv);
  290. ui::keybind_close_manual_end(siv, false);
  291. } else {
  292. // Invalid
  293. let language = get_appdata(siv).language;
  294. ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
  295. ui::error(
  296. siv,
  297. ui::Labels::InvalidUsernameExplination.localize(language),
  298. );
  299. }
  300. })
  301. .button(ui::Labels::Cancel.localize(language), |siv| {
  302. ui::keybind_close_manual_end(siv, false);
  303. })
  304. .min_size(DIALOGUE_MIN_SIZE),
  305. );
  306. })
  307. .with_name(USERNAME_BUTTON_ID);
  308. // Refresh button
  309. let refresh_button = Button::new("", move |siv| {
  310. if let Err(e) = actions::load_messages(siv) {
  311. let language = get_appdata(siv).language;
  312. ui::error(siv, ui::Labels::FailedToLoadMessages(e).localize(language));
  313. }
  314. ui::visual_update(siv);
  315. })
  316. .with_name(REFRESH_BUTTON_ID);
  317. // Blocked words list
  318. let blocked_words_list_button = {
  319. Button::new("", move |siv| {
  320. let appdata = get_appdata(siv);
  321. let language = appdata.language;
  322. let wordslist_id = utils::new_id();
  323. let wordslist_id_clone = wordslist_id.clone();
  324. let wordslist_id_typingarea_clone = wordslist_id.clone();
  325. let mut wordslist_view =
  326. SelectView::<String>::new().on_submit(move |siv, word: &str| {
  327. siv.with_user_data(|appdata: &mut Appdata| {
  328. appdata.blocked_phrases.retain(|x| x != word);
  329. });
  330. siv.call_on_name(&wordslist_id_clone, |view: &mut NamedView<SelectView>| {
  331. let idx = view
  332. .get_mut()
  333. .selected_id()
  334. .expect("Selection should exist when fired from submit.");
  335. view.get_mut().remove_item(idx);
  336. });
  337. save_appdata(siv);
  338. ui::visual_update(siv);
  339. });
  340. wordslist_view.add_all_str(&appdata.blocked_phrases);
  341. let typingarea_id = utils::new_id();
  342. let typing_area = EditView::new().with_name(&typingarea_id);
  343. let typing_button = Button::new(ui::Labels::Submit.localize(language), move |siv| {
  344. let text = siv
  345. .call_on_name(&typingarea_id, |view: &mut EditView| {
  346. let s = utils::strings::insensitive_string(&view.get_content());
  347. view.set_content("");
  348. s
  349. })
  350. .map(|x| x.to_string())
  351. .unwrap_or("".to_string());
  352. if get_appdata(siv).blocked_phrases.contains(&text) || text.is_empty() {
  353. return;
  354. }
  355. siv.with_user_data(|appdata: &mut Appdata| {
  356. appdata.blocked_phrases.push(text.clone());
  357. });
  358. siv.call_on_name(
  359. &wordslist_id_typingarea_clone,
  360. |view: &mut NamedView<SelectView>| {
  361. view.get_mut().add_item_str(text);
  362. },
  363. );
  364. save_appdata(siv);
  365. ui::visual_update(siv);
  366. });
  367. let d = Dialog::new()
  368. .title(ui::Labels::BlockedWords.localize(language))
  369. .button(ui::Labels::Close.localize(language), |siv| {
  370. ui::keybind_close_manual_end(siv, false);
  371. })
  372. .content(
  373. LinearLayout::vertical()
  374. .child(ScrollView::new(wordslist_view.with_name(wordslist_id)))
  375. .child(DummyView.fixed_height(1))
  376. .child(
  377. LinearLayout::horizontal()
  378. .child(typing_area.min_width(DIALOGUE_MIN_SIZE.0 as usize))
  379. .child(typing_button),
  380. ),
  381. );
  382. ui::keybind_setup_close_once(siv);
  383. siv.add_layer(d);
  384. })
  385. .with_name(ui::BLOCKED_WORDS_BUTTON_ID)
  386. };
  387. // Server settings
  388. let server_settings_button = Button::new("", |siv| {
  389. let appdata = get_appdata(siv);
  390. let language = appdata.language;
  391. ui::keybind_setup_close_once(siv);
  392. siv.add_layer(
  393. cursive::views::Dialog::new()
  394. .title(ui::Labels::ServerSettings.localize(language))
  395. .content(
  396. LinearLayout::vertical()
  397. .child(
  398. LinearLayout::horizontal()
  399. .child(
  400. TextView::new(ui::Labels::ServerAddress.localize(language))
  401. .fixed_width(ui::SERVER_SETTINGS_FIELD_SIZE.0 / 2),
  402. )
  403. .child(DummyView.fixed_width(1))
  404. .child(
  405. EditView::new()
  406. .content(appdata.api_endpoint)
  407. .with_name(ui::SERVER_SETTINGS_ADDRESS_FIELD_ID)
  408. .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
  409. .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
  410. ),
  411. )
  412. .child(
  413. LinearLayout::horizontal()
  414. .child(
  415. TextView::new(ui::Labels::ServerRefreshRate.localize(language))
  416. .fixed_width(ui::SERVER_SETTINGS_FIELD_SIZE.0 / 2),
  417. )
  418. .child(DummyView.fixed_width(1))
  419. .child(
  420. EditView::new()
  421. .content(appdata.api_refresh_rate.to_string())
  422. .with_name(ui::SERVER_SETTINGS_REFRESH_FIELD_ID)
  423. .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
  424. .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
  425. ),
  426. )
  427. .child(
  428. LinearLayout::horizontal()
  429. .child(
  430. TextView::new(ui::Labels::Password.localize(language))
  431. .fixed_width(ui::SERVER_SETTINGS_FIELD_SIZE.0 / 2),
  432. )
  433. .child(DummyView.fixed_width(1))
  434. .child(
  435. EditView::new()
  436. .content(appdata.api_password.to_string())
  437. .with_name(ui::SERVER_SETTINGS_PASSWORD_FIELD_ID)
  438. .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
  439. .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
  440. ),
  441. ),
  442. )
  443. .button(ui::Labels::Ok.localize(language), |siv| {
  444. let input_addr = siv
  445. .call_on_name(
  446. ui::SERVER_SETTINGS_ADDRESS_FIELD_ID,
  447. |view: &mut EditView| view.get_content(),
  448. )
  449. .expect("Failed to retrieve server settings address input.");
  450. let input_refresh = siv
  451. .call_on_name(
  452. ui::SERVER_SETTINGS_REFRESH_FIELD_ID,
  453. |view: &mut EditView| view.get_content(),
  454. )
  455. .expect("Failed to retrieve server settings refresh rate input.");
  456. let input_password = siv
  457. .call_on_name(
  458. ui::SERVER_SETTINGS_PASSWORD_FIELD_ID,
  459. |view: &mut EditView| view.get_content(),
  460. )
  461. .expect("Failed to retrieve server settings password input.");
  462. siv.with_user_data(|appdata: &mut Appdata| {
  463. appdata.api_endpoint = input_addr.to_string();
  464. appdata.api_password = input_password.to_string();
  465. if let Ok(rate) = input_refresh.parse() {
  466. appdata.api_refresh_rate = rate;
  467. }
  468. });
  469. save_appdata(siv);
  470. ui::visual_update(siv);
  471. ui::keybind_close_manual_end(siv, false);
  472. })
  473. .button(ui::Labels::Cancel.localize(language), |siv| {
  474. ui::keybind_close_manual_end(siv, false);
  475. })
  476. .min_size(DIALOGUE_MIN_SIZE),
  477. );
  478. })
  479. .with_name(ui::SERVER_SETTINGS_BUTTON_ID);
  480. // Main layout
  481. let main_layout = LinearLayout::vertical()
  482. .child(
  483. LinearLayout::horizontal()
  484. .child(
  485. LinearLayout::vertical()
  486. .child(channels_panel.fixed_width(USER_PANEL_SIZE).full_height())
  487. .child(channel_current_panel),
  488. )
  489. .child(
  490. LinearLayout::vertical()
  491. .child(users_panel.resized(
  492. cursive::view::SizeConstraint::Fixed(USER_PANEL_SIZE),
  493. cursive::view::SizeConstraint::Full,
  494. ))
  495. .child(current_username_panel),
  496. )
  497. .child(messages_panel.full_width()),
  498. )
  499. .child(input_panel)
  500. .child(
  501. LinearLayout::horizontal()
  502. .child(language_button)
  503. .child(DummyView.full_width())
  504. .child(username_button)
  505. .child(DummyView.full_width())
  506. .child(channels_new_button)
  507. .child(DummyView.full_width())
  508. .child(blocked_words_list_button)
  509. .child(DummyView.full_width())
  510. .child(server_settings_button)
  511. .child(DummyView.full_width())
  512. .child(refresh_button)
  513. .full_width(),
  514. );
  515. siv.add_fullscreen_layer(main_layout);
  516. siv.run();
  517. }