From 5e95d2b62c18f0acf5d3e69a6aa2a4a55a2036ca Mon Sep 17 00:00:00 2001 From: Neshura Date: Wed, 21 Jun 2023 21:29:14 +0200 Subject: [PATCH] Static Example TUI --- Cargo.lock | 96 ++++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 255 +++++++++++++++++++++++++++++++++------------------- 3 files changed, 261 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a6490a..874d303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,7 @@ name = "ascendance-of-a-bookworm-bot" version = "1.1.0" dependencies = [ "chrono", + "crossterm 0.26.1", "lemmy_api_common", "lemmy_db_schema", "once_cell", @@ -101,6 +102,7 @@ dependencies = [ "serde_derive", "serde_json", "strum_macros 0.25.0", + "tui", "url", ] @@ -203,6 +205,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.79" @@ -266,6 +274,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -772,6 +821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -1164,6 +1214,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1478,6 +1549,19 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.25.0", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "typed-builder" version = "0.10.0" @@ -1510,6 +1594,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "url" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index c93197e..3ceaa82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chrono = "0.4.26" +crossterm = "0.26.1" lemmy_api_common = "0.17.4" lemmy_db_schema = "0.17.4" once_cell = "1.18.0" @@ -15,4 +16,5 @@ serde = "1.0.164" serde_derive = "1.0.164" serde_json = "1.0.97" strum_macros = "0.25.0" +tui = "0.19.0" url = "2.4.0" diff --git a/src/main.rs b/src/main.rs index 617c333..5b628ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,9 @@ use lemmy_db_schema::{ }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; -use std::{thread::sleep, time}; +use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Widget, Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; +use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},}; +use std::{thread::{sleep, self}, time::{self, Duration}, io}; use url::Url; use crate::config::FeedData; @@ -144,95 +146,164 @@ impl CommunitiesVector { } } -fn main() { - // Get all needed auth tokens at the start - let mut old = Utc::now().time(); - let mut this = Bot::new(); - println!("{}", this.secrets.lemmy.username); - this.login(); - this.community_ids.load(&this.auth, &this.config.instance); - - // Create empty eTag list - println!("TODO: Etag list"); - - // Enter a loop (not for debugging) - loop { - let start = Utc::now(); - print!("\x1B[2J\x1B[1;1H"); - println!( - "Started loop at {} {}", - start.format("%H:%M:%S"), - start.timezone() - ); - - if start.time() - old > chrono::Duration::seconds(6) { - old = start.time(); - this.config = Config::load(); - this.community_ids.load(&this.auth, &this.config.instance); - } - - // Start the polling process - // Get all feed URLs (use cache) - let mut post_queue: Vec = vec![]; - this.config.feeds.iter().for_each(|feed| { - let res = CLIENT - .get(feed.feed_url.clone()) - .send() - .unwrap() - .text() - .unwrap(); - let data: FeedData = serde_json::from_str(&res).unwrap(); - - let mut prev_post_idx: Option = None; - let mut do_post = true; - this.post_history - .iter() - .enumerate() - .for_each(|(idx, post)| { - if &post.last_post_url == &data.items[0].url { - do_post = false; - } else if &post.title == &data.title { - prev_post_idx = Some(idx); - } - }); - - if do_post { - let item = &data.items[0]; - let new_post = CreatePost { - name: item.title.clone(), - community_id: this.community_ids.find(&feed.communities.chapter), - url: Some(Url::parse(&item.url).unwrap()), - body: Some( - "[Reddit](https://reddit.com)\n\n[Discord](https://discord.com)".into(), - ), - honeypot: None, - nsfw: Some(false), - language_id: Some(LanguageId(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future - auth: this.auth.clone(), - }; - post_queue.push(new_post); - match prev_post_idx { - Some(idx) => { - this.post_history[idx].title = data.title; - this.post_history[idx].last_post_url = item.url.clone(); - } - None => this.post_history.push(PrevPost { - title: data.title, - last_post_url: item.url.clone(), - }), - } - } - sleep(time::Duration::from_millis(100)); // Should prevent dos-ing J-Novel servers - }); - - PrevPost::save(&this.post_history); - post_queue.iter().for_each(|post| { - println!("Posting: {}", post.name); - this.post(post.clone()); - }); - - while Utc::now().time() - start.time() < chrono::Duration::seconds(60) { - sleep(time::Duration::from_secs(10)); - } - } +fn run_bot() { + // Get all needed auth tokens at the start + let mut old = Utc::now().time(); + let mut this = Bot::new(); + println!("{}", this.secrets.lemmy.username); + this.login(); + this.community_ids.load(&this.auth, &this.config.instance); + + // Create empty eTag list + println!("TODO: Etag list"); + + // Enter a loop (not for debugging) + loop { + let start = Utc::now(); + print!("\x1B[2J\x1B[1;1H"); + println!( + "Started loop at {} {}", + start.format("%H:%M:%S"), + start.timezone() + ); + + if start.time() - old > chrono::Duration::seconds(6) { + old = start.time(); + this.config = Config::load(); + this.community_ids.load(&this.auth, &this.config.instance); + } + + // Start the polling process + // Get all feed URLs (use cache) + let mut post_queue: Vec = vec![]; + this.config.feeds.iter().for_each(|feed| { + let res = CLIENT + .get(feed.feed_url.clone()) + .send() + .unwrap() + .text() + .unwrap(); + let data: FeedData = serde_json::from_str(&res).unwrap(); + + let mut prev_post_idx: Option = None; + let mut do_post = true; + this.post_history + .iter() + .enumerate() + .for_each(|(idx, post)| { + if &post.last_post_url == &data.items[0].url { + do_post = false; + } else if &post.title == &data.title { + prev_post_idx = Some(idx); + } + }); + + if do_post { + let item = &data.items[0]; + let new_post = CreatePost { + name: item.title.clone(), + community_id: this.community_ids.find(&feed.communities.chapter), + url: Some(Url::parse(&item.url).unwrap()), + body: Some( + "[Reddit](https://reddit.com)\n\n[Discord](https://discord.com)".into(), + ), + honeypot: None, + nsfw: Some(false), + language_id: Some(LanguageId(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future + auth: this.auth.clone(), + }; + post_queue.push(new_post); + match prev_post_idx { + Some(idx) => { + this.post_history[idx].title = data.title; + this.post_history[idx].last_post_url = item.url.clone(); + } + None => this.post_history.push(PrevPost { + title: data.title, + last_post_url: item.url.clone(), + }), + } + } + sleep(time::Duration::from_millis(100)); // Should prevent dos-ing J-Novel servers + }); + + PrevPost::save(&this.post_history); + post_queue.iter().for_each(|post| { + println!("Posting: {}", post.name); + this.post(post.clone()); + }); + + while Utc::now().time() - start.time() < chrono::Duration::seconds(60) { + sleep(time::Duration::from_secs(10)); + } + } +} + +fn ui(f: &mut Frame) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(10), + Constraint::Percentage(90), + ].as_ref() + ) + .split(f.size()); + + + let block = Block::default() + .title("Block") + .borders(Borders::ALL); + f.render_widget(block, chunks[0]); + + let selected_style = Style::default().add_modifier(Modifier::REVERSED); + let normal_style = Style::default().bg(Color::Blue); + let header_cells = ["Series", "Community", "Last Post"] + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red))); + let header = Row::new(header_cells) + .style(normal_style) + .height(1) + .bottom_margin(1); + let rows = [ + Row::new([Cell::from("a1"), Cell::from("a2"), Cell::from("a3")]).height(1 as u16).bottom_margin(1), + Row::new([Cell::from("b1"), Cell::from("b2"), Cell::from("b3")]).height(1 as u16).bottom_margin(1), + Row::new([Cell::from("unicorn"), Cell::from("c2"), Cell::from("c3")]).height(2 as u16).bottom_margin(1) + ]; + + let t = Table::new(rows) + .header(header) + .block(Block::default().borders(Borders::ALL).title("Table")) + .highlight_style(selected_style) + .highlight_symbol(">> ") + .widths(&[ + Constraint::Percentage(50), + Constraint::Length(30), + Constraint::Min(10), + ]); + f.render_widget(t, chunks[1]); +} + +fn main() -> Result<(), io::Error> { + let stdout = io::stdout(); + let backend = CrosstermBackend::new(&stdout); + let mut terminal = Terminal::new(backend)?; + + terminal.draw(|f| { + ui(f); + })?; + + thread::sleep(Duration::from_secs(5)); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + Ok(()) + //run_bot(); }