├── .gitignore ├── docs └── demo.gif ├── rustfmt.toml ├── src ├── client │ ├── response.rs │ ├── mod.rs │ └── api.rs ├── keybinds │ ├── file.rs │ ├── mod.rs │ └── key.rs ├── event.rs ├── model.rs ├── style.rs ├── format.rs ├── app.rs └── main.rs ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuqqu/tui-chan/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_field_init_shorthand = true 2 | 3 | # nightly 4 | #condense_wildcard_suffixes = true 5 | #imports_granularity = "Module" 6 | #group_imports = "StdExternalCrate" -------------------------------------------------------------------------------- /src/client/response.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::model::{Board, Thread, ThreadPost}; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub(super) struct BoardListResponse { 7 | pub(super) boards: Vec, 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub(super) struct ThreadListResponse { 12 | pub(super) threads: Vec, 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | pub(super) struct ThreadResponse { 17 | pub(super) posts: Vec, 18 | } 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tui-chan" 3 | version = "0.5.0" 4 | authors = ["tuqqu "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | reqwest = { version = "0.12.7", features = ["json", "native-tls-alpn"] } 12 | tokio = { version = "1.36.0", features = ["full"] } 13 | serde_json = "1.0" 14 | serde = { version = "1.0", features = ["derive"] } 15 | tui = "0.16" 16 | termion = "1.5" 17 | chrono = "0.4" 18 | htmlescape="0.3" 19 | regex="1.4" 20 | voca_rs="1.13" 21 | clipboard = "0.5" 22 | open = "1" 23 | 24 | [profile.dev] 25 | opt-level = 0 26 | 27 | [profile.release] 28 | opt-level = 3 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Arthur Kurbidaev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/keybinds/file.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, io, path::Path}; 2 | 3 | use super::Keybinds; 4 | 5 | /// Read keybinds file in config directory as string, and create new file if it does not exist 6 | pub fn read_or_create_keybinds_file() -> Result { 7 | // Find config folder or use default 8 | let Ok(config) = get_config_folder() else { 9 | eprintln!("Could not find home config folder file. Continuing with default config."); 10 | return Ok(Keybinds::default_file_contents()); 11 | }; 12 | 13 | let folder = format!("{config}/tui-chan"); 14 | let filepath = format!("{folder}/keybinds.conf"); 15 | 16 | // Create folder if it does not exist (non-recursive) 17 | if !Path::new(&folder).exists() { 18 | fs::create_dir(&folder)?; 19 | } 20 | 21 | // Create file if it does not exist 22 | if !Path::new(&filepath).exists() { 23 | let default_contents = Keybinds::default_file_contents(); 24 | fs::write(&filepath, &default_contents)?; 25 | // Return contents 26 | return Ok(default_contents); 27 | } 28 | 29 | // Read file 30 | fs::read_to_string(&filepath) 31 | } 32 | 33 | /// Get config home folder for Linux 34 | fn get_config_folder() -> Result { 35 | env::var("XDG_CONFIG_HOME") 36 | .or_else(|_| env::var("HOME").map(|home| format!("{}/.config", home))) 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: CI 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | 26 | fmt: 27 | name: Rustfmt 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: stable 35 | override: true 36 | - run: rustup component add rustfmt 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: fmt 40 | args: --all -- --check 41 | 42 | clippy: 43 | name: Clippy 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | override: true 52 | - run: rustup component add clippy 53 | - uses: actions-rs/cargo@v1 54 | with: 55 | command: clippy 56 | args: -- -D warnings -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use reqwest::Client; 4 | 5 | use crate::client::api::ApiUrlProvider; 6 | use crate::client::response::{BoardListResponse, ThreadListResponse, ThreadResponse}; 7 | use crate::model::{Board, Thread, ThreadPost}; 8 | 9 | pub(crate) mod api; 10 | mod response; 11 | 12 | pub(crate) struct ChanClient { 13 | client: Client, 14 | api: &'static dyn ApiUrlProvider, 15 | } 16 | 17 | type ClientResult = Result>; 18 | 19 | impl ChanClient { 20 | pub(crate) fn new(client: Client, api: &'static dyn ApiUrlProvider) -> Self { 21 | Self { api, client } 22 | } 23 | 24 | pub(crate) async fn get_boards(&self) -> ClientResult> { 25 | let boards_response: BoardListResponse = self 26 | .client 27 | .get(self.api.boards()) 28 | .send() 29 | .await? 30 | .json::() 31 | .await?; 32 | 33 | Ok(boards_response.boards) 34 | } 35 | 36 | pub(crate) async fn get_threads(&self, board: &str, page: u8) -> ClientResult> { 37 | let threads_response: ThreadListResponse = self 38 | .client 39 | .get(self.api.threads(board, page)) 40 | .send() 41 | .await? 42 | .json::() 43 | .await?; 44 | 45 | Ok(threads_response.threads) 46 | } 47 | 48 | pub(crate) async fn get_thread(&self, board: &str, no: u64) -> ClientResult> { 49 | let thread_response: ThreadResponse = self 50 | .client 51 | .get(self.api.thread(board, no)) 52 | .send() 53 | .await? 54 | .json::() 55 | .await?; 56 | 57 | Ok(thread_response.posts) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tui-chan 2 | An Imageboard Terminal User Interface. 3 | Currently supports only 4chan. 4 | 5 | ![demo](docs/demo.gif) 6 | 7 | ## Installation 8 | Download the [latest release][latest-releases]. The binary executable is `tui-chan`. Put it in your PATH so that you can execute it from everywhere. 9 | 10 | Then run it from the command line. 11 | ```shell 12 | tui-chan 13 | ``` 14 | 15 | You may specify an imageboard name as an argument, the default one is `4chan`. 16 | 17 | ## Building from source 18 | If your architecture is not supported by the pre-built binaries you can build the application from the source code yourself. 19 | Make sure you have [Rust][rust-installation-url] installed. 20 | 21 | ```shell 22 | git clone https://github.com/tuqqu/tui-chan.git 23 | cd tui-chan 24 | cargo install --path . # copies binary to /.cargo/bin/ 25 | 26 | # to uninstall run 27 | cargo uninstall 28 | ``` 29 | 30 | ## Controls 31 | 32 | Controls can be configured in `~/.config/tui-chan/keybinds.conf` 33 | 34 | ### Default controls 35 | 36 | Press `h` to show / hide help bar to look up controls. 37 | Use `d` to open board or thread and `a` to return to the previous panel. 38 | 39 | | Description | Keys | 40 | |------------------------------------------------------|-------------------------------| 41 | | Move around | `w`,`a`,`s`,`d` | 42 | | Move quickly | control + `w`,`a`,`s`,`d` | 43 | | Toggle help bar | `h` | 44 | | Next page | `p` | 45 | | Previous page | control + `p` | 46 | | Reload page | `r` | 47 | | Toggle fullscreen for the selected panel | `z` | 48 | | Copy the direct url to the selected thread or post | `c` | 49 | | Copy the selected post media (image/webm) url | control + `c` | 50 | | Open the selected thread or post in browser | `o` | 51 | | Open the selected post media (image/webm) in browser | control + `o` | 52 | | Quit | `q` | 53 | 54 | [latest-releases]: https://github.com/tuqqu/tui-chan/releases 55 | [rust-installation-url]: https://www.rust-lang.org/tools/install 56 | -------------------------------------------------------------------------------- /src/client/api.rs: -------------------------------------------------------------------------------- 1 | pub trait ApiUrlProvider { 2 | fn boards(&self) -> String; 3 | 4 | fn threads(&self, board: &str, page: u8) -> String; 5 | 6 | fn thread(&self, board: &str, no: u64) -> String; 7 | } 8 | 9 | pub(crate) trait ContentUrlProvider { 10 | fn url_board(&self, board: &str) -> String; 11 | 12 | fn url_thread(&self, board: &str, no: u64) -> String; 13 | 14 | fn url_thread_post(&self, board: &str, no: u64, post_no: u64) -> String; 15 | 16 | fn url_file(&self, board: &str, filename: String) -> String; 17 | } 18 | 19 | pub(crate) trait ChannelProvider: ContentUrlProvider + ApiUrlProvider { 20 | fn as_api(&self) -> &dyn ApiUrlProvider; 21 | 22 | fn as_content(&self) -> &dyn ContentUrlProvider; 23 | } 24 | 25 | pub(crate) struct Api4chan; 26 | 27 | impl Api4chan { 28 | const NAME: &'static str = "4chan"; 29 | const BASE_API_URL: &'static str = "https://a.4cdn.org"; 30 | const BASE_URL: &'static str = "https://boards.4chan.org"; 31 | const BASE_MEDIA_URL: &'static str = "https://i.4cdn.org"; 32 | } 33 | 34 | impl ApiUrlProvider for Api4chan { 35 | fn boards(&self) -> String { 36 | format!("{}/boards.json", Self::BASE_API_URL) 37 | } 38 | 39 | fn threads(&self, board: &str, page: u8) -> String { 40 | format!("{}/{}/{}.json", Self::BASE_API_URL, board, page) 41 | } 42 | 43 | fn thread(&self, board: &str, no: u64) -> String { 44 | format!("{}/{}/thread/{}.json", Self::BASE_API_URL, board, no) 45 | } 46 | } 47 | 48 | impl ContentUrlProvider for Api4chan { 49 | fn url_board(&self, board: &str) -> String { 50 | format!("{}/{}/", Self::BASE_URL, board) 51 | } 52 | 53 | fn url_thread(&self, board: &str, no: u64) -> String { 54 | format!("{}/{}/thread/{}", Self::BASE_URL, board, no) 55 | } 56 | 57 | fn url_thread_post(&self, board: &str, no: u64, post_no: u64) -> String { 58 | format!("{}/{}/thread/{}#p{}", Self::BASE_URL, board, no, post_no) 59 | } 60 | 61 | fn url_file(&self, board: &str, filename: String) -> String { 62 | format!("{}/{}/{}", Self::BASE_MEDIA_URL, board, filename) 63 | } 64 | } 65 | 66 | impl ChannelProvider for Api4chan { 67 | fn as_api(&self) -> &dyn ApiUrlProvider { 68 | self 69 | } 70 | 71 | fn as_content(&self) -> &dyn ContentUrlProvider { 72 | self 73 | } 74 | } 75 | 76 | const DEFAULT_API: &str = "default"; 77 | 78 | pub(crate) fn from_name(name: &str) -> Option<&'static dyn ChannelProvider> { 79 | match name { 80 | DEFAULT_API | Api4chan::NAME => Some(&Api4chan {}), 81 | _ => None, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | use std::sync::{mpsc, Arc}; 3 | use std::time::Duration; 4 | use std::{io, thread}; 5 | 6 | use termion::event::Key; 7 | use termion::input::TermRead; 8 | 9 | pub(crate) struct Events { 10 | rx: mpsc::Receiver>, 11 | _input_handle: thread::JoinHandle<()>, 12 | _ignore_exit_key: Arc, 13 | _tick_handle: thread::JoinHandle<()>, 14 | } 15 | 16 | pub(crate) enum Event { 17 | Input(I), 18 | Tick, 19 | } 20 | 21 | impl Events { 22 | pub(crate) fn new() -> Events { 23 | Events::with_config(Config::default()) 24 | } 25 | 26 | fn with_config(config: Config) -> Events { 27 | let (tx, rx) = mpsc::channel(); 28 | let ignore_exit_key = Arc::new(AtomicBool::new(false)); 29 | let input_handle = { 30 | let tx = tx.clone(); 31 | let ignore_exit_key = ignore_exit_key.clone(); 32 | thread::spawn(move || { 33 | let stdin = io::stdin(); 34 | for key in stdin.keys().flatten() { 35 | if let Err(err) = tx.send(Event::Input(key)) { 36 | eprintln!("{}", err); 37 | return; 38 | } 39 | if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { 40 | return; 41 | } 42 | } 43 | }) 44 | }; 45 | 46 | let tick_handle = { 47 | thread::spawn(move || loop { 48 | if tx.send(Event::Tick).is_err() { 49 | break; 50 | } 51 | thread::sleep(config.tick_rate); 52 | }) 53 | }; 54 | 55 | Events { 56 | rx, 57 | _ignore_exit_key: ignore_exit_key, 58 | _input_handle: input_handle, 59 | _tick_handle: tick_handle, 60 | } 61 | } 62 | 63 | pub(crate) fn next(&self) -> Result, mpsc::RecvError> { 64 | self.rx.recv() 65 | } 66 | 67 | pub fn _disable_exit_key(&mut self) { 68 | self._ignore_exit_key.store(true, Ordering::Relaxed); 69 | } 70 | 71 | pub fn _enable_exit_key(&mut self) { 72 | self._ignore_exit_key.store(false, Ordering::Relaxed); 73 | } 74 | } 75 | 76 | #[derive(Debug, Clone, Copy)] 77 | struct Config { 78 | exit_key: Key, 79 | tick_rate: Duration, 80 | } 81 | 82 | impl Default for Config { 83 | fn default() -> Config { 84 | Config { 85 | exit_key: Key::Char('q'), 86 | tick_rate: Duration::from_millis(250), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use crate::format::format_html; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub(crate) struct Board { 6 | board: String, 7 | title: String, 8 | meta_description: String, 9 | per_page: isize, 10 | pages: isize, 11 | bump_limit: isize, 12 | } 13 | 14 | impl Board { 15 | pub(crate) fn board(&self) -> &str { 16 | &self.board 17 | } 18 | 19 | pub(crate) fn title(&self) -> &str { 20 | &self.title 21 | } 22 | 23 | pub(crate) fn meta_description(&self) -> &str { 24 | &self.meta_description 25 | } 26 | 27 | #[allow(dead_code)] 28 | pub(crate) fn per_page(&self) -> isize { 29 | self.per_page 30 | } 31 | 32 | #[allow(dead_code)] 33 | pub(crate) fn pages(&self) -> isize { 34 | self.pages 35 | } 36 | 37 | #[allow(dead_code)] 38 | pub(crate) fn bump_limit(&self) -> isize { 39 | self.bump_limit 40 | } 41 | } 42 | 43 | pub struct ThreadList { 44 | page: u8, 45 | description: String, 46 | } 47 | 48 | impl ThreadList { 49 | const DEFAULT: u8 = 1; 50 | 51 | pub(crate) fn new() -> Self { 52 | Self { 53 | page: Self::DEFAULT, 54 | description: "".to_string(), 55 | } 56 | } 57 | 58 | pub(crate) fn next_page(&mut self, board: &Board) -> u8 { 59 | if board.pages as u8 == self.page { 60 | self.page = Self::DEFAULT; 61 | } else { 62 | self.page += 1; 63 | } 64 | 65 | self.page 66 | } 67 | 68 | pub(crate) fn prev_page(&mut self, board: &Board) -> u8 { 69 | if Self::DEFAULT == self.page { 70 | self.page = board.pages as u8; 71 | } else { 72 | self.page -= 1; 73 | } 74 | 75 | self.page 76 | } 77 | 78 | pub(crate) fn cur_page(&self) -> u8 { 79 | self.page 80 | } 81 | 82 | pub(crate) fn set_description(&mut self, desc: &str) { 83 | self.description = format_html(desc); 84 | } 85 | 86 | pub(crate) fn description(&self) -> &str { 87 | &self.description 88 | } 89 | } 90 | 91 | #[derive(Debug, Serialize, Deserialize)] 92 | pub struct Thread { 93 | posts: Vec, 94 | } 95 | 96 | impl Thread { 97 | pub(crate) fn posts(&self) -> &[ThreadPost] { 98 | &self.posts 99 | } 100 | } 101 | 102 | #[derive(Debug, Serialize, Deserialize)] 103 | pub struct ThreadPost { 104 | #[serde(default)] 105 | no: usize, 106 | #[serde(default)] 107 | now: String, 108 | #[serde(default)] 109 | time: u64, 110 | #[serde(default)] 111 | id: String, 112 | #[serde(default)] 113 | name: String, 114 | #[serde(default)] 115 | com: String, 116 | #[serde(default)] 117 | sub: String, 118 | #[serde(default)] 119 | sticky: u8, 120 | #[serde(default)] 121 | closed: u8, 122 | #[serde(default)] 123 | replies: u32, 124 | #[serde(default)] 125 | ext: Option, 126 | #[serde(default)] 127 | filename: Option, 128 | #[serde(default)] 129 | tim: Option, 130 | } 131 | 132 | impl ThreadPost { 133 | pub(crate) fn no(&self) -> usize { 134 | self.no 135 | } 136 | 137 | pub(crate) fn time(&self) -> u64 { 138 | self.time 139 | } 140 | 141 | pub(crate) fn name(&self) -> &str { 142 | &self.name 143 | } 144 | 145 | pub(crate) fn com(&self) -> &str { 146 | &self.com 147 | } 148 | 149 | pub(crate) fn sub(&self) -> &str { 150 | &self.sub 151 | } 152 | 153 | pub(crate) fn sticky(&self) -> u8 { 154 | self.sticky 155 | } 156 | 157 | pub(crate) fn closed(&self) -> u8 { 158 | self.closed 159 | } 160 | 161 | pub(crate) fn replies(&self) -> u32 { 162 | self.replies 163 | } 164 | 165 | pub(crate) fn ext(&self) -> &Option { 166 | &self.ext 167 | } 168 | 169 | pub(crate) fn filename(&self) -> &Option { 170 | &self.filename 171 | } 172 | 173 | pub(crate) fn tim(&self) -> Option { 174 | self.tim 175 | } 176 | 177 | #[allow(dead_code)] 178 | pub(crate) fn now(&self) -> &str { 179 | &self.now 180 | } 181 | 182 | #[allow(dead_code)] 183 | pub(crate) fn id(&self) -> &str { 184 | &self.id 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | use tui::style::Color; 2 | use tui::widgets::BorderType; 3 | 4 | pub(crate) struct StyleProvider { 5 | highlight_color: Color, 6 | default_color: Color, 7 | highlight_border_type: BorderType, 8 | default_border_type: BorderType, 9 | highlight_border_color: Color, 10 | default_border_color: Color, 11 | } 12 | 13 | impl StyleProvider { 14 | pub(crate) fn new() -> Self { 15 | Self { 16 | highlight_color: Color::DarkGray, 17 | default_color: Color::Reset, 18 | highlight_border_type: BorderType::Plain, 19 | default_border_type: BorderType::Plain, 20 | highlight_border_color: Color::Blue, 21 | default_border_color: Color::Reset, 22 | } 23 | } 24 | 25 | pub(crate) fn default_from_selected_field(&self, selected_field: &SelectedField) -> BlockStyle { 26 | BlockStyle::from_selected_field( 27 | selected_field, 28 | self.highlight_color, 29 | self.default_color, 30 | self.highlight_border_type, 31 | self.default_border_type, 32 | self.highlight_border_color, 33 | self.default_border_color, 34 | ) 35 | } 36 | 37 | pub(crate) fn highlight_color(&self) -> &Color { 38 | &self.highlight_color 39 | } 40 | } 41 | 42 | pub(crate) enum SelectedField { 43 | BoardList, 44 | ThreadList, 45 | Thread, 46 | } 47 | 48 | pub(crate) struct BlockBorderColor { 49 | board_list: Color, 50 | thread_list: Color, 51 | thread: Color, 52 | } 53 | 54 | impl BlockBorderColor { 55 | fn new(board_list: Color, thread_list: Color, thread: Color) -> Self { 56 | Self { 57 | board_list, 58 | thread_list, 59 | thread, 60 | } 61 | } 62 | 63 | fn from_selected_field( 64 | selected_field: &SelectedField, 65 | highlight_color: Color, 66 | default_color: Color, 67 | ) -> Self { 68 | match selected_field { 69 | SelectedField::BoardList => Self::new(highlight_color, default_color, default_color), 70 | SelectedField::ThreadList => Self::new(default_color, highlight_color, default_color), 71 | SelectedField::Thread => Self::new(default_color, default_color, highlight_color), 72 | } 73 | } 74 | 75 | pub(crate) fn board_list(&self) -> Color { 76 | self.board_list 77 | } 78 | 79 | pub(crate) fn thread_list(&self) -> Color { 80 | self.thread_list 81 | } 82 | 83 | pub(crate) fn thread(&self) -> Color { 84 | self.thread 85 | } 86 | } 87 | 88 | pub(crate) struct BlockBorderType { 89 | board_list: BorderType, 90 | thread_list: BorderType, 91 | thread: BorderType, 92 | } 93 | 94 | impl BlockBorderType { 95 | fn new(board_list: BorderType, thread_list: BorderType, thread: BorderType) -> Self { 96 | Self { 97 | board_list, 98 | thread_list, 99 | thread, 100 | } 101 | } 102 | 103 | fn from_selected_field( 104 | selected_field: &SelectedField, 105 | highlight_border: BorderType, 106 | default_border: BorderType, 107 | ) -> Self { 108 | match selected_field { 109 | SelectedField::BoardList => Self::new(highlight_border, default_border, default_border), 110 | SelectedField::ThreadList => { 111 | Self::new(default_border, highlight_border, default_border) 112 | } 113 | SelectedField::Thread => Self::new(default_border, default_border, highlight_border), 114 | } 115 | } 116 | 117 | pub(crate) fn board_list(&self) -> BorderType { 118 | self.board_list 119 | } 120 | 121 | pub(crate) fn thread_list(&self) -> BorderType { 122 | self.thread_list 123 | } 124 | 125 | pub(crate) fn thread(&self) -> BorderType { 126 | self.thread 127 | } 128 | } 129 | 130 | pub(crate) struct BlockStyle { 131 | border_color: BlockBorderColor, 132 | border_type: BlockBorderType, 133 | } 134 | 135 | impl BlockStyle { 136 | pub(crate) fn from_selected_field( 137 | selected_field: &SelectedField, 138 | _highlight_color: Color, 139 | _default_color: Color, 140 | highlight_border_type: BorderType, 141 | default_border_type: BorderType, 142 | highlight_border_color: Color, 143 | default_border_color: Color, 144 | ) -> Self { 145 | Self::new( 146 | BlockBorderColor::from_selected_field( 147 | selected_field, 148 | highlight_border_color, 149 | default_border_color, 150 | ), 151 | BlockBorderType::from_selected_field( 152 | selected_field, 153 | highlight_border_type, 154 | default_border_type, 155 | ), 156 | ) 157 | } 158 | 159 | fn new(border_color: BlockBorderColor, border_type: BlockBorderType) -> Self { 160 | Self { 161 | border_color, 162 | border_type, 163 | } 164 | } 165 | 166 | pub(crate) fn border_color(&self) -> &BlockBorderColor { 167 | &self.border_color 168 | } 169 | 170 | pub(crate) fn border_type(&self) -> &BlockBorderType { 171 | &self.border_type 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/keybinds/mod.rs: -------------------------------------------------------------------------------- 1 | mod file; 2 | mod key; 3 | 4 | pub use self::file::read_or_create_keybinds_file; 5 | pub use self::key::{display_key, ParseErrorKind}; 6 | 7 | use std::collections::HashMap; 8 | use termion::event::Key; 9 | 10 | use self::key::parse_keybind; 11 | 12 | // Creates `pub struct Keybinds` 13 | macro_rules! define_keybinds { 14 | { $( 15 | $name:ident // ID 16 | $($mod:ident)? $key:literal // DEFAULT KEYBIND 17 | #[doc = $desc:literal] // DESCRIPTION 18 | )* $(,)? } => { 19 | /// Keybind configuration 20 | #[derive(Debug)] 21 | pub struct Keybinds { $( 22 | #[doc = $desc] 23 | pub $name: Key, 24 | )* } 25 | 26 | impl Keybinds { 27 | /// Parse keybinds from configuration file (`.conf`) 28 | /// 29 | /// Uses default value if keybind not given 30 | pub fn parse_from_file(file: &str) -> Result { 31 | 32 | // Get key/value pairs as hashmap 33 | let mut keymap = parse_keymap_file(file)?; 34 | 35 | // Construct self 36 | Ok(Self { $( 37 | $name: 38 | // Use default value, if not defined in file 39 | keymap.remove(stringify!($name)) 40 | .unwrap_or_else(|| define_keybinds!(@modifier $($mod)?)($key)), 41 | )* }) 42 | } 43 | 44 | /// Get contents of keybind file, with default configuration 45 | pub fn default_file_contents() -> String { 46 | let mut contents = String::from("# Keybinds for tui-chan\n# https://github.com/tuqqu/tui-chan\n\n"); 47 | 48 | $( 49 | let key = define_keybinds!(@modifier $($mod)?)($key); 50 | 51 | contents += &format!("#{}\n{}={}\n", 52 | $desc, 53 | stringify!($name), 54 | display_key(&key), 55 | ); 56 | )* 57 | 58 | contents 59 | } 60 | } 61 | }; 62 | 63 | // Use `Char` if no modifier given 64 | (@modifier $mod:ident) => { Key::$mod }; 65 | (@modifier ) => { Key::Char }; 66 | } 67 | 68 | define_keybinds! { 69 | // ID DEFAULT DESCRIPTION 70 | up 'w' /// Move up 71 | down 's' /// Move down 72 | left 'a' /// Move left 73 | right 'd' /// Move right 74 | quick_up Ctrl 'w' /// Move up quickly 75 | quick_down Ctrl 's' /// Move down quickly 76 | quick_left Ctrl 'a' /// Move left quickly 77 | quick_right Ctrl 'd' /// Move right quickly 78 | page_next 'p' /// Next page 79 | page_previous Ctrl 'p' /// Previous page 80 | copy_thread 'c' /// Copy the direct url to the selected thread or post 81 | open_thread 'o' /// Open the selected thread or post in browser 82 | copy_media Ctrl 'c' /// Copy the selected post media (image/webm) url 83 | open_media Ctrl 'o' /// Open the selected post media (image/webm) in browser 84 | fullscreen 'z' /// Toggle fullscreen for the selected panel 85 | reload 'r' /// Reload page 86 | help 'h' /// Toggle help bar 87 | quit 'q' /// Quit 88 | // Default `quit` keybind must also be updated in `event.rs` 89 | } 90 | 91 | /// Error parsing keybind configuration file 92 | #[derive(Debug)] 93 | #[allow(dead_code)] 94 | pub enum KeybindsError { 95 | /// Failed to parse single keybind 96 | Parse { 97 | /// Line number 98 | line_no: usize, 99 | /// Parsing error kind 100 | kind: ParseErrorKind, 101 | /// Name of keybind 102 | name: String, 103 | /// Keybind value 104 | keybind: String, 105 | }, 106 | 107 | /// Keybind was already defined in file 108 | KeybindAlreadyDefined { 109 | /// Line number 110 | line_no: usize, 111 | /// Name of keybind 112 | name: String, 113 | }, 114 | 115 | /// No name in keybind definition 116 | NoName { 117 | /// Line number 118 | line_no: usize, 119 | }, 120 | 121 | /// No value in keybind definition 122 | NoValue { 123 | /// Line number 124 | line_no: usize, 125 | }, 126 | } 127 | 128 | /// Map keybind name to key 129 | type KeyMap<'a> = HashMap<&'a str, Key>; 130 | 131 | /// Parse keybinds file, as hashmap 132 | fn parse_keymap_file(file: &str) -> Result { 133 | let mut keymap = KeyMap::new(); 134 | 135 | // Loop lines 136 | for (line_no, line) in file.lines().enumerate() { 137 | let line_no = line_no + 1; 138 | 139 | // Ignore blank lines and comments 140 | if line.is_empty() || line.starts_with('#') { 141 | continue; 142 | } 143 | 144 | let mut split = line.split('='); 145 | 146 | // Name of keybind 147 | let name = split 148 | .next() 149 | .filter(|name| !name.is_empty()) 150 | .ok_or(KeybindsError::NoName { line_no })? 151 | .trim(); 152 | 153 | // Check name not already defined 154 | if keymap.contains_key(name) { 155 | return Err(KeybindsError::KeybindAlreadyDefined { 156 | line_no, 157 | name: name.to_string(), 158 | }); 159 | } 160 | 161 | // Value of keybind 162 | let keybind = split 163 | .next() 164 | .filter(|name| !name.is_empty()) 165 | .ok_or(KeybindsError::NoValue { line_no })? 166 | .trim(); 167 | 168 | // Parse as `Key` 169 | let key = parse_keybind(keybind).map_err(|kind| KeybindsError::Parse { 170 | line_no, 171 | kind, 172 | name: name.to_string(), 173 | keybind: keybind.to_string(), 174 | })?; 175 | 176 | keymap.insert(name, key); 177 | } 178 | 179 | Ok(keymap) 180 | } 181 | -------------------------------------------------------------------------------- /src/keybinds/key.rs: -------------------------------------------------------------------------------- 1 | use termion::event::Key; 2 | 3 | /// Parse (deserialize) keybind string as `termion::event::Key`. 4 | /// 5 | /// Include modifer key, by separating with a space ('Ctrl a'). 6 | /// Valid modifier keys include 'Ctrl' and 'Alt'. 7 | /// 8 | /// To use 'Shift' with characters, use capitalized form ('Ctrl A', not 'Ctrl Shift a') 9 | /// 10 | /// Space is used as separator, because plus ('+') can be used as key name. 11 | pub fn parse_keybind(keybind: &str) -> Result { 12 | let mut parts = keybind.split(' ').rev(); 13 | 14 | // Last part is key name (must exist) 15 | let Some(keyname) = parts.next().filter(|str| !str.is_empty()) else { 16 | return Err(ParseErrorKind::MissingKeyName); 17 | }; 18 | 19 | // Optional modifier, next from end 20 | let modifier = parts.next().filter(|str| !str.is_empty()); 21 | 22 | // Anything before that is invalid 23 | if parts.next().is_some() { 24 | return Err(ParseErrorKind::TooManyModifiers); 25 | } 26 | 27 | // One character in keyname 28 | if let Some(ch) = keyname.chars().next() { 29 | if keyname.len() == 1 { 30 | // Check character is valid ASCII letter, number or symbol (not space) 31 | if !(33 as char..=126 as char).contains(&ch) { 32 | return Err(ParseErrorKind::InvalidCharacterKeyName); 33 | } 34 | 35 | // No modifier 36 | let Some(modifier) = modifier else { 37 | return Ok(Key::Char(ch)); 38 | }; 39 | 40 | // Use valid modifier 41 | let key = match modifier.to_lowercase().as_str() { 42 | "ctrl" => Key::Ctrl(ch), 43 | "alt" => Key::Alt(ch), 44 | _ => return Err(ParseErrorKind::UnknownModifier), 45 | }; 46 | 47 | return Ok(key); 48 | } 49 | } 50 | 51 | // Cannot use modifier with special key 52 | if modifier.is_some() { 53 | return Err(ParseErrorKind::ModifierWithSpecialKey); 54 | } 55 | 56 | // Use valid special key name 57 | let key = match keyname.to_lowercase().as_str() { 58 | "backspace" => Key::Backspace, 59 | "left" => Key::Left, 60 | "right" => Key::Right, 61 | "up" => Key::Up, 62 | "down" => Key::Down, 63 | "home" => Key::Home, 64 | "end" => Key::End, 65 | "pageup" => Key::PageUp, 66 | "pagedown" => Key::PageDown, 67 | "backtab" => Key::BackTab, 68 | "delete" => Key::Delete, 69 | "insert" => Key::Insert, 70 | "esc" => Key::Esc, 71 | 72 | _ => return Err(ParseErrorKind::InvalidSpecialKeyName), 73 | }; 74 | 75 | Ok(key) 76 | } 77 | 78 | /// Stringify (serialize) key, using same format to parse keybind 79 | pub fn display_key(key: &Key) -> String { 80 | match key { 81 | Key::Char(ch) => ch.to_string(), 82 | Key::Ctrl(ch) => format!("Ctrl {ch}"), 83 | Key::Alt(ch) => format!("Alt {ch}"), 84 | // Mirrors the match statement in `parse_keybind` 85 | Key::Backspace => String::from("Backspace"), 86 | Key::Left => String::from("Left"), 87 | Key::Right => String::from("Right"), 88 | Key::Up => String::from("Up"), 89 | Key::Down => String::from("Down"), 90 | Key::Home => String::from("Home"), 91 | Key::End => String::from("End"), 92 | Key::PageUp => String::from("PageUp"), 93 | Key::PageDown => String::from("PageDown"), 94 | Key::BackTab => String::from("BackTab"), 95 | Key::Delete => String::from("Delete"), 96 | Key::Insert => String::from("Insert"), 97 | Key::Esc => String::from("Esc"), 98 | 99 | _ => unreachable!("Trying to serialize `Key` which should never exist"), 100 | } 101 | } 102 | 103 | /// Error parsing keybind 104 | #[derive(Debug, PartialEq)] 105 | pub enum ParseErrorKind { 106 | /// No key name was found in keybind 107 | MissingKeyName, 108 | /// Keyname character is not between ASCII 33-126. 109 | /// 110 | /// Valid characters include all ASCII letters, numbers, and symbols, 111 | /// but not space, control characters, or multi-byte unicode characters 112 | InvalidCharacterKeyName, 113 | /// Invalid name for 'special key', such as 'Backspace' or 'Up' 114 | InvalidSpecialKeyName, 115 | /// Too many modifier keys are in keybind. 116 | /// 117 | /// To use 'Shift' with characters, use capitalized form ('Ctrl A', not 'Ctrl Shift a') 118 | TooManyModifiers, 119 | /// Modifier key is not valid. 120 | /// 121 | /// Valid modifier keys include 'Ctrl' and 'Alt' 122 | /// 123 | /// To use 'Shift' with characters, use capitalized form ('Ctrl A', not 'Ctrl Shift a') 124 | UnknownModifier, 125 | /// Modifier cannot be used with 'special key', such as 'Backspace' or 'Up' 126 | ModifierWithSpecialKey, 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | 133 | #[test] 134 | fn parse_keybind_works() { 135 | use parse_keybind as parse; 136 | use ParseErrorKind::*; 137 | 138 | // Ok 139 | 140 | assert_eq!(parse("a"), Ok(Key::Char('a'))); 141 | assert_eq!(parse("A"), Ok(Key::Char('A'))); 142 | assert_eq!(parse("Ctrl a"), Ok(Key::Ctrl('a'))); 143 | assert_eq!(parse("Ctrl A"), Ok(Key::Ctrl('A'))); 144 | assert_eq!(parse("Alt z"), Ok(Key::Alt('z'))); 145 | assert_eq!(parse("["), Ok(Key::Char('['))); 146 | assert_eq!(parse("!"), Ok(Key::Char('!'))); 147 | assert_eq!(parse("~"), Ok(Key::Char('~'))); 148 | assert_eq!(parse("Alt ^"), Ok(Key::Alt('^'))); 149 | assert_eq!(parse("Ctrl 6"), Ok(Key::Ctrl('6'))); 150 | assert_eq!(parse("Backspace"), Ok(Key::Backspace)); 151 | assert_eq!(parse("Up"), Ok(Key::Up)); 152 | 153 | // Err 154 | 155 | assert_eq!(parse(""), Err(MissingKeyName)); 156 | assert_eq!(parse(" "), Err(MissingKeyName)); 157 | assert_eq!(parse(" "), Err(MissingKeyName)); 158 | assert_eq!(parse("a "), Err(MissingKeyName)); 159 | 160 | assert_eq!(parse("Ctrl Shift a"), Err(TooManyModifiers)); 161 | assert_eq!(parse("Alt a"), Err(TooManyModifiers)); 162 | assert_eq!(parse(" a"), Err(TooManyModifiers)); 163 | 164 | assert_eq!( 165 | parse(&(1 as char).to_string()), 166 | Err(InvalidCharacterKeyName) 167 | ); 168 | 169 | assert_eq!(parse("Shift a"), Err(UnknownModifier)); 170 | assert_eq!(parse("f a"), Err(UnknownModifier)); 171 | 172 | assert_eq!(parse("Ctrl Backspace"), Err(ModifierWithSpecialKey)); 173 | assert_eq!(parse("Ctrl Shift"), Err(ModifierWithSpecialKey)); 174 | 175 | assert_eq!(parse("ä"), Err(InvalidSpecialKeyName)); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, UNIX_EPOCH}; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use tui::layout::Rect; 5 | use tui::style::{Color, Modifier, Style}; 6 | use tui::text::{Span, Spans, Text}; 7 | use tui::widgets::ListItem; 8 | use voca_rs::strip; 9 | 10 | use crate::model::ThreadPost; 11 | 12 | pub(crate) fn format_default(str: &str) -> String { 13 | format!(" {}", str) 14 | } 15 | 16 | pub(crate) fn format_html(str: &str) -> String { 17 | htmlescape::decode_html(str).unwrap() 18 | } 19 | 20 | pub(crate) fn format_post_short(post: &ThreadPost, no: usize, len: usize, area: Rect) -> ListItem { 21 | format_post(post, format!("{}/{}", no, len), area, true) 22 | } 23 | 24 | pub(crate) fn format_post_full(post: &ThreadPost, no: usize, area: Rect) -> ListItem { 25 | format_post(post, format!("#{}", no), area, false) 26 | } 27 | 28 | const CUT_MSG: &str = "[...]"; 29 | const CUT_MSG_LEN: usize = CUT_MSG.len(); 30 | 31 | const LIMIT_SHORT: usize = 10; 32 | const LIMIT_LONG: usize = 60; 33 | 34 | fn format_post(post: &ThreadPost, no: String, area: Rect, short: bool) -> ListItem { 35 | let mut lines = vec![Spans::from("")]; 36 | let mut header: Vec = vec![]; 37 | 38 | if !post.sub().is_empty() { 39 | header.push(Span::styled( 40 | format_default(&htmlescape::decode_html(post.sub()).unwrap()), 41 | Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), 42 | )); 43 | } 44 | 45 | header.push(Span::raw(" ")); 46 | header.push(Span::styled( 47 | format!( 48 | "{} {} No.{}", 49 | htmlescape::decode_html(post.name()).unwrap(), 50 | format_time(post.time()), 51 | post.no(), 52 | ), 53 | Style::default().add_modifier(Modifier::ITALIC | Modifier::UNDERLINED), 54 | )); 55 | 56 | header.push(Span::styled( 57 | format_default(&no), 58 | Style::default().fg(Color::Yellow), 59 | )); 60 | 61 | if post.sticky() == 1 { 62 | header.push(Span::styled(format_default("📌"), Style::default())); 63 | } 64 | 65 | if post.closed() == 1 { 66 | header.push(Span::styled(format_default("🔓"), Style::default())); 67 | } 68 | 69 | lines.push(Spans::from(header)); 70 | 71 | if post.filename().is_some() && post.ext().is_some() { 72 | lines.push(Spans::from(Span::styled( 73 | format_default(&format!( 74 | "{}{}", 75 | post.filename().as_ref().unwrap(), 76 | post.ext().as_ref().unwrap() 77 | )), 78 | Style::default() 79 | .fg(Color::Cyan) 80 | .add_modifier(Modifier::ITALIC), 81 | ))); 82 | } 83 | 84 | let cut_com = format_post_contents( 85 | post.com(), 86 | calc_width(area) as usize, 87 | if short { LIMIT_SHORT } else { LIMIT_LONG }, 88 | ); 89 | for span in cut_com { 90 | lines.push(span); 91 | } 92 | 93 | if short { 94 | lines.push(Spans::from(Span::styled( 95 | format_default(&format!("{} Replies", post.replies())), 96 | Style::default() 97 | .fg(Color::Magenta) 98 | .add_modifier(Modifier::ITALIC), 99 | ))); 100 | } 101 | 102 | lines.push(Spans::from("")); 103 | ListItem::new(Text::from(lines)).style(Style::default()) 104 | } 105 | 106 | fn format_post_contents(string: &str, sub_len: usize, line_limit: usize) -> Vec { 107 | let string = htmlescape::decode_html(string).unwrap(); 108 | let split = string.split("
"); 109 | let lines: Vec<&str> = split.collect(); 110 | 111 | let mut spans = Vec::with_capacity(sub_len * line_limit); 112 | let mut i = 0; 113 | 114 | 'line_loop: for line in lines { 115 | let line = strip::strip_tags(line); 116 | let line_type = LineType::from_line(&line); 117 | 118 | let mut iter = line.chars(); 119 | let strlen = line.len(); 120 | let mut pos = 0; 121 | 122 | if strlen == 0 { 123 | spans.push(Spans::from("")); 124 | 125 | i += 1; 126 | 127 | if i >= line_limit { 128 | break; 129 | } 130 | } 131 | 132 | while pos < strlen { 133 | let len = iter 134 | .by_ref() 135 | .take(sub_len) 136 | .fold(0, |acc, ch| acc + ch.len_utf8()); 137 | 138 | if i >= line_limit { 139 | spans.push(Spans::from(vec![ 140 | Span::styled(format_default(cut_line(&line, pos, len)), line_type.style()), 141 | Span::styled(CUT_MSG, Style::default().fg(Color::Magenta)), 142 | ])); 143 | break 'line_loop; 144 | } 145 | 146 | spans.push(Spans::from(Span::styled( 147 | format_default(&line[pos..pos + len]), 148 | line_type.style(), 149 | ))); 150 | 151 | pos += len; 152 | i += 1; 153 | } 154 | } 155 | 156 | spans 157 | } 158 | 159 | fn format_time(timestamp: u64) -> String { 160 | let st = UNIX_EPOCH + Duration::from_secs(timestamp); 161 | let datetime = DateTime::::from(st); 162 | 163 | datetime.format("%m/%d/%y(%a)%H:%M:%S").to_string() 164 | } 165 | 166 | fn cut_line(line: &str, pos: usize, cur_len: usize) -> &str { 167 | let cut = if cur_len < CUT_MSG_LEN { 168 | cur_len 169 | } else { 170 | CUT_MSG_LEN 171 | }; 172 | 173 | &line[pos..pos + cur_len - cut] 174 | } 175 | 176 | fn calc_width(area: Rect) -> u16 { 177 | const MIN_WIDTH: i16 = 10; 178 | const BORDERS: i16 = 4; 179 | 180 | let area_width = area.width as i16; 181 | let width = if area_width - BORDERS < MIN_WIDTH { 182 | MIN_WIDTH 183 | } else { 184 | area_width - BORDERS 185 | }; 186 | 187 | width as u16 188 | } 189 | 190 | #[derive(Default)] 191 | enum LineType { 192 | #[default] 193 | Text, 194 | Greentext, 195 | Reply, 196 | } 197 | 198 | impl LineType { 199 | fn from_line(line: &str) -> Self { 200 | let first = line.chars().next(); 201 | let second = line.chars().nth(1); 202 | 203 | match (first, second) { 204 | (Some('>'), Some('>')) => Self::Reply, 205 | (Some('>'), _) => Self::Greentext, 206 | _ => Self::default(), 207 | } 208 | } 209 | 210 | fn style(&self) -> Style { 211 | match self { 212 | Self::Text => Style::default(), 213 | Self::Greentext => Style::default().fg(Color::Green), 214 | Self::Reply => Style::default().fg(Color::Yellow), 215 | } 216 | } 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use super::*; 222 | 223 | #[test] 224 | fn test_format_time() { 225 | assert_eq!(format_time(1617810439), "04/07/21(Wed)15:47:19"); 226 | assert_eq!(format_time(1717810439), "06/08/24(Sat)01:33:59"); 227 | } 228 | 229 | #[test] 230 | fn test_format_default() { 231 | assert_eq!(format_default("string"), " string"); 232 | } 233 | 234 | #[test] 235 | fn test_format_post_contents() { 236 | const POST: &str = "Natus est Schubert Himmelpfortgrund in vico Alsergrund Vindobonae \ 237 | die 31 Ianuarii 1797. Pater, Franciscus Theodorus Schubert, filius pagani Moraviani, \ 238 | magister scholae paroechialis; mater, Elisabeth (Vietz), filia artificis claustrarii \ 239 | Silesici fuit, quae ante nuptias ut ancilla in familia Vindobonensi laboraverat."; 240 | 241 | // untruncated post formatting 242 | assert_eq!(format_post_contents(POST, 100, 5), vec![ 243 | Spans::from(" Natus est Schubert Himmelpfortgrund in vico Alsergrund Vindobonae die 31 Ianuarii 1797. Pater, Franc"), 244 | Spans::from(" iscus Theodorus Schubert, filius pagani Moraviani, magister scholae paroechialis; mater, Elisabeth ("), 245 | Spans::from(" Vietz), filia artificis claustrarii Silesici fuit, quae ante nuptias ut ancilla in familia Vindobone"), 246 | Spans::from(" nsi laboraverat."), 247 | ]); 248 | 249 | // truncated post formatting 250 | assert_eq!( 251 | format_post_contents(POST, 50, 2), 252 | vec![ 253 | Spans::from(" Natus est Schubert Himmelpfortgrund in vico Alserg"), 254 | Spans::from(" rund Vindobonae die 31 Ianuarii 1797. Pater, Franc"), 255 | Spans::from(vec![ 256 | Span::from(" iscus Theodorus Schubert, filius pagani Morav"), 257 | Span::styled("[...]", Style::default().fg(Color::Magenta)) 258 | ]), 259 | ] 260 | ); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use tui::widgets::ListState; 2 | 3 | use crate::client::api::ContentUrlProvider; 4 | use crate::format::format_html; 5 | use crate::keybinds::{display_key, Keybinds}; 6 | use crate::model::{Board, Thread, ThreadPost}; 7 | use crate::style::SelectedField; 8 | 9 | pub(crate) struct App { 10 | pub(crate) boards: ItemLIst, 11 | pub(crate) threads: ItemLIst, 12 | pub(crate) thread: ItemLIst, 13 | shown_state: ShownState, 14 | help_bar: HelpBar, 15 | } 16 | 17 | /// Format 2D array as table, with aligned columns 18 | fn format_table(data: &[&[&str]]) -> String { 19 | // Find the maximum length of each column 20 | let mut max_lengths = vec![0; data[0].len()]; 21 | for row in data { 22 | for (i, &cell) in row.iter().enumerate() { 23 | max_lengths[i] = max_lengths[i].max(cell.len()); 24 | } 25 | } 26 | // Compile table 27 | let mut rows = Vec::new(); 28 | for row in data { 29 | let mut cells = Vec::new(); 30 | for (i, &cell) in row.iter().enumerate() { 31 | cells.push(format!("{:, 41 | threads: Vec, 42 | thread: Vec, 43 | keybinds: &Keybinds, 44 | ) -> Self { 45 | /// Get keybinds as strings 46 | macro_rules! get_keys { 47 | ( $($name:ident),* $(,)? ) => { 48 | $( let $name = display_key(&keybinds.$name);)* 49 | } 50 | } 51 | get_keys![ 52 | up, 53 | down, 54 | left, 55 | right, 56 | quick_up, 57 | quick_down, 58 | quick_left, 59 | quick_right, 60 | page_next, 61 | page_previous, 62 | copy_thread, 63 | open_thread, 64 | copy_media, 65 | open_media, 66 | fullscreen, 67 | reload, 68 | help, 69 | quit, 70 | ]; 71 | 72 | // Create table of keybinds 73 | let table: &[&[&str]] = &[ 74 | &[ 75 | "move around:", 76 | &format!("{up}, {down}, {left}, {right}"), 77 | "toggle help bar:", 78 | &help, 79 | ], 80 | &[ 81 | "move quickly:", 82 | &format!("{quick_up}, {quick_down}, {quick_left}, {quick_right}"), 83 | "copy thread/post url:", 84 | ©_thread, 85 | ], 86 | &[ 87 | "toggle fullscreen:", 88 | &fullscreen, 89 | "copy media url:", 90 | ©_media, 91 | ], 92 | &[ 93 | "next page:", 94 | &page_next, 95 | "open thread/post in browser", 96 | &open_thread, 97 | ], 98 | &["previous page:", &page_previous, "reload page:", &reload], 99 | &["quit:", &quit, "open media url in browser:", &open_media], 100 | ]; 101 | 102 | let text = format!( 103 | r##" 104 | {table} 105 | Controls can be changed in ~/.config/tui-chan/keybinds.conf 106 | Note: to enter the board/thread use "{right}" 107 | "##, 108 | table = format_table(table) 109 | ); 110 | 111 | Self { 112 | boards: ItemLIst::new(boards), 113 | threads: ItemLIst::new(threads), 114 | thread: ItemLIst::new(thread), 115 | shown_state: ShownState { 116 | board_list: false, 117 | thread_list: false, 118 | thread: false, 119 | }, 120 | help_bar: HelpBar { 121 | shown: false, 122 | title: format!("Help (\"{help}\" to toggle)"), 123 | text, 124 | }, 125 | } 126 | } 127 | 128 | pub(crate) fn fill_threads(&mut self, threads: Vec) { 129 | self.threads = ItemLIst::new(threads); 130 | } 131 | 132 | pub(crate) fn fill_thread(&mut self, thread: Vec) { 133 | self.thread = ItemLIst::new(thread); 134 | } 135 | 136 | pub(crate) fn advance_idly(&self) {} 137 | 138 | pub(crate) fn advance(&mut self, selected_field: &SelectedField, steps: isize) { 139 | match selected_field { 140 | SelectedField::BoardList => { 141 | self.boards.advance_by(steps); 142 | } 143 | SelectedField::ThreadList => { 144 | self.threads.advance_by(steps); 145 | } 146 | SelectedField::Thread => { 147 | self.thread.advance_by(steps); 148 | } 149 | }; 150 | } 151 | 152 | pub(crate) fn calc_screen_share(&self) -> ScreenShare { 153 | match ( 154 | self.shown_state.board_list, 155 | self.shown_state.thread_list, 156 | self.shown_state.thread, 157 | ) { 158 | (true, false, false) => ScreenShare::new(100, 0, 0), 159 | (true, true, false) => ScreenShare::new(12, 88, 0), 160 | (true, true, true) => ScreenShare::new(12, 88, 50), // check 161 | (false, true, true) => ScreenShare::new(12, 34, 54), 162 | (false, false, true) => ScreenShare::new(0, 0, 100), 163 | (false, true, false) => ScreenShare::new(0, 100, 0), 164 | _ => ScreenShare::new(100, 0, 0), 165 | } 166 | } 167 | 168 | pub(crate) fn selected_board(&self) -> &Board { 169 | &self.boards.items[self.boards.state.selected().unwrap_or(0)] 170 | } 171 | 172 | pub(crate) fn selected_thread(&self) -> &Thread { 173 | &self.threads.items[self.threads.state.selected().unwrap_or(0)] 174 | } 175 | 176 | pub(crate) fn selected_thread_description(&self) -> String { 177 | if let Some(post_i) = self.threads.state.selected() { 178 | let thread = &self.threads.items[post_i]; 179 | let post = thread.posts().first().unwrap(); 180 | let title = format_html(post.sub()); 181 | let title = if title.is_empty() { 182 | "".to_string() 183 | } else { 184 | format!("\"{}\" ", title) 185 | }; 186 | 187 | format!("{} {}replies: {} ", post.no(), title, post.replies()) 188 | } else { 189 | "".to_string() 190 | } 191 | } 192 | 193 | pub(crate) fn selected_post(&self) -> &ThreadPost { 194 | &self.thread.items[self.thread.state.selected().unwrap()] 195 | } 196 | 197 | pub(crate) fn set_shown_board_list(&mut self, shown: bool) { 198 | self.shown_state.board_list = shown; 199 | } 200 | 201 | pub(crate) fn set_shown_thread_list(&mut self, shown: bool) { 202 | self.shown_state.thread_list = shown; 203 | } 204 | 205 | pub(crate) fn set_shown_thread(&mut self, shown: bool) { 206 | self.shown_state.thread = shown; 207 | } 208 | 209 | pub(crate) fn toggle_shown_board_list(&mut self) { 210 | self.shown_state.board_list ^= true; 211 | } 212 | 213 | pub(crate) fn toggle_shown_thread_list(&mut self) { 214 | self.shown_state.thread_list ^= true; 215 | } 216 | 217 | #[allow(dead_code)] 218 | pub(crate) fn toggle_shown_thread(&mut self) { 219 | self.shown_state.thread ^= true; 220 | } 221 | 222 | #[allow(dead_code)] 223 | pub(crate) fn shown_board_list(&mut self) -> bool { 224 | self.shown_state.board_list 225 | } 226 | 227 | pub(crate) fn shown_thread_list(&mut self) -> bool { 228 | self.shown_state.thread_list 229 | } 230 | 231 | pub(crate) fn shown_thread(&mut self) -> bool { 232 | self.shown_state.thread 233 | } 234 | 235 | pub(crate) fn help_bar(&self) -> &HelpBar { 236 | &self.help_bar 237 | } 238 | 239 | pub(crate) fn help_bar_mut(&mut self) -> &mut HelpBar { 240 | &mut self.help_bar 241 | } 242 | 243 | pub(crate) fn url_boards(&self, url_provider: &dyn ContentUrlProvider) -> String { 244 | url_provider.url_board(self.selected_board().board()) 245 | } 246 | 247 | pub(crate) fn url_threads(&self, url_provider: &dyn ContentUrlProvider) -> String { 248 | url_provider.url_thread( 249 | self.selected_board().board(), 250 | self.selected_thread().posts().first().unwrap().no() as u64, 251 | ) 252 | } 253 | 254 | pub(crate) fn url_thread(&self, url_provider: &dyn ContentUrlProvider) -> String { 255 | url_provider.url_thread_post( 256 | self.selected_board().board(), 257 | self.selected_thread().posts().first().unwrap().no() as u64, 258 | self.selected_post().no() as u64, 259 | ) 260 | } 261 | 262 | pub(crate) fn media_url_threads( 263 | &self, 264 | url_provider: &dyn ContentUrlProvider, 265 | ) -> Option { 266 | let post = self.selected_thread().posts().first().unwrap(); 267 | self.media_url(post, url_provider) 268 | } 269 | 270 | pub(crate) fn media_url_thread(&self, url_provider: &dyn ContentUrlProvider) -> Option { 271 | let post = self.selected_post(); 272 | self.media_url(post, url_provider) 273 | } 274 | 275 | fn media_url( 276 | &self, 277 | post: &ThreadPost, 278 | url_provider: &dyn ContentUrlProvider, 279 | ) -> Option { 280 | if post.tim().is_none() || post.ext().is_none() { 281 | return None; 282 | } 283 | 284 | let url = url_provider.url_file( 285 | self.selected_board().board(), 286 | format!( 287 | "{}{}", 288 | post.tim().as_ref().unwrap(), 289 | post.ext().as_ref().unwrap() 290 | ), 291 | ); 292 | 293 | Some(url) 294 | } 295 | } 296 | 297 | pub(crate) struct ScreenShare { 298 | board_list: u16, 299 | thread_list: u16, 300 | thread: u16, 301 | } 302 | 303 | impl ScreenShare { 304 | fn new(board_list: u16, thread_list: u16, thread: u16) -> ScreenShare { 305 | ScreenShare { 306 | board_list, 307 | thread_list, 308 | thread, 309 | } 310 | } 311 | 312 | pub(crate) fn board_list(&self) -> u16 { 313 | self.board_list 314 | } 315 | 316 | pub(crate) fn thread_list(&self) -> u16 { 317 | self.thread_list 318 | } 319 | 320 | pub(crate) fn thread(&self) -> u16 { 321 | self.thread 322 | } 323 | } 324 | 325 | struct ShownState { 326 | board_list: bool, 327 | thread_list: bool, 328 | thread: bool, 329 | } 330 | 331 | pub(crate) struct ItemLIst { 332 | pub(crate) state: ListState, 333 | pub(crate) items: Vec, 334 | } 335 | 336 | pub(crate) struct HelpBar { 337 | shown: bool, 338 | title: String, 339 | text: String, 340 | } 341 | 342 | impl HelpBar { 343 | pub(crate) fn shown(&self) -> bool { 344 | self.shown 345 | } 346 | 347 | pub(crate) fn toggle_shown(&mut self) { 348 | self.shown ^= true; 349 | } 350 | 351 | pub(crate) fn title(&self) -> &String { 352 | &self.title 353 | } 354 | 355 | pub(crate) fn text(&self) -> &String { 356 | &self.text 357 | } 358 | } 359 | 360 | impl ItemLIst { 361 | pub(crate) fn new(items: Vec) -> ItemLIst { 362 | ItemLIst { 363 | state: ListState::default(), 364 | items, 365 | } 366 | } 367 | 368 | pub(crate) fn advance_by(&mut self, steps: isize) { 369 | let selected = match self.state.selected() { 370 | Some(selected) => { 371 | if selected as isize >= self.items.len() as isize - steps { 372 | 0_isize 373 | } else if selected == 0 && steps < 0 { 374 | self.items.len() as isize - 1 375 | } else { 376 | selected as isize + steps 377 | } 378 | } 379 | None => 0, 380 | }; 381 | 382 | self.state.select(Some(selected as usize)); 383 | } 384 | 385 | pub(crate) fn _unselect(&mut self) { 386 | self.state.select(None); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::single_match)] 2 | 3 | use std::{env, io, process, str}; 4 | 5 | use client::ChanClient; 6 | use clipboard::{ClipboardContext, ClipboardProvider}; 7 | use open::that as open_in_browser; 8 | use reqwest::Client; 9 | use termion::input::MouseTerminal; 10 | use termion::raw::IntoRawMode; 11 | use termion::screen::AlternateScreen; 12 | use tokio::runtime::Runtime; 13 | use tui::backend::TermionBackend; 14 | use tui::layout::{Constraint, Direction, Layout}; 15 | use tui::style::{Color, Modifier, Style}; 16 | use tui::text::{Span, Spans}; 17 | use tui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}; 18 | use tui::Terminal; 19 | 20 | use crate::app::App; 21 | use crate::client::api::{ 22 | from_name as channel_provider_from_name, ChannelProvider, ContentUrlProvider, 23 | }; 24 | use crate::event::{Event, Events}; 25 | use crate::format::{format_default, format_post_full, format_post_short}; 26 | use crate::keybinds::{read_or_create_keybinds_file, Keybinds}; 27 | use crate::model::{Board, Thread, ThreadList, ThreadPost}; 28 | use crate::style::{SelectedField, StyleProvider}; 29 | 30 | mod app; 31 | mod client; 32 | mod event; 33 | mod format; 34 | mod keybinds; 35 | mod model; 36 | mod style; 37 | 38 | fn main() -> Result<(), io::Error> { 39 | // Get keybinds from config file 40 | let keybinds = read_or_create_keybinds_file().expect("Failed to read keybinds file"); 41 | let keybinds = Keybinds::parse_from_file(&keybinds).expect("Failed to parse keybinds file"); 42 | 43 | let stdout = io::stdout().into_raw_mode()?; 44 | let stdout = MouseTerminal::from(stdout); 45 | let stdout = AlternateScreen::from(stdout); 46 | let backend = TermionBackend::new(stdout); 47 | let mut terminal = Terminal::new(backend)?; 48 | let runtime = Runtime::new()?; 49 | 50 | let args: Vec = env::args().collect(); 51 | let chan: &str = if args.len() == 1 { "default" } else { &args[1] }; 52 | 53 | let api: &dyn ChannelProvider = match channel_provider_from_name(chan) { 54 | Some(api) => api, 55 | None => { 56 | println!("Imageboard name \"{}\" is not valid.", chan); 57 | process::exit(1); 58 | } 59 | }; 60 | 61 | let client = ChanClient::new(Client::new(), api.as_api()); 62 | let events = Events::new(); 63 | let api: &dyn ContentUrlProvider = api.as_content(); 64 | 65 | let mut boards: Vec = vec![]; 66 | runtime.block_on(async { 67 | let result = client.get_boards().await; 68 | 69 | match result { 70 | Ok(data) => boards = data, 71 | Err(_) => panic!("Could not fetch boards"), 72 | }; 73 | }); 74 | 75 | let mut app = App::new(boards, vec![], vec![], &keybinds); 76 | app.set_shown_board_list(true); 77 | let mut selected_field: SelectedField = SelectedField::BoardList; 78 | let mut thread_list = ThreadList::new(); 79 | let style_prov = StyleProvider::new(); 80 | let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 81 | 82 | loop { 83 | terminal.draw(|f| { 84 | let block_style = style_prov.default_from_selected_field(&selected_field); 85 | let scr_share = app.calc_screen_share(); 86 | 87 | let mut constraints = vec![Constraint::Min(0)]; 88 | if app.help_bar().shown() { 89 | constraints.push(Constraint::Length(10)); 90 | } 91 | 92 | let helpbar_chunk = Layout::default() 93 | .constraints::<&[Constraint]>(constraints.as_ref()) 94 | .split(f.size()); 95 | 96 | if app.help_bar().shown() { 97 | let block = Block::default().borders(Borders::NONE).title(Span::styled( 98 | app.help_bar().title(), 99 | Style::default() 100 | .fg(Color::Magenta) 101 | .add_modifier(Modifier::BOLD), 102 | )); 103 | let paragraph = Paragraph::new(app.help_bar().text().as_str()) 104 | .block(block) 105 | .wrap(Wrap { trim: true }); 106 | f.render_widget(paragraph, helpbar_chunk[1]); 107 | } 108 | 109 | let chunks = Layout::default() 110 | .direction(Direction::Horizontal) 111 | .constraints( 112 | [ 113 | Constraint::Percentage(scr_share.board_list()), 114 | Constraint::Percentage(scr_share.thread_list()), 115 | Constraint::Percentage(scr_share.thread()), 116 | ] 117 | .as_ref(), 118 | ) 119 | .split(helpbar_chunk[0]); 120 | 121 | let items: Vec = app 122 | .boards 123 | .items 124 | .iter() 125 | .map(|board| { 126 | let lines = vec![Spans::from(vec![ 127 | Span::styled( 128 | format_default(&format!("/{}/", board.board())), 129 | Style::default().fg(Color::Magenta), 130 | ), 131 | Span::raw(format_default(board.title())), 132 | ])]; 133 | 134 | ListItem::new(lines).style(Style::default()) 135 | }) 136 | .collect(); 137 | 138 | let items = List::new(items) 139 | .block( 140 | Block::default() 141 | .borders(Borders::ALL) 142 | .border_style(Style::default().fg(block_style.border_color().board_list())) 143 | .border_type(block_style.border_type().board_list()) 144 | .title(format_default("Boards ")), 145 | ) 146 | .highlight_style( 147 | Style::default() 148 | .bg(*style_prov.highlight_color()) 149 | .add_modifier(Modifier::BOLD), 150 | ); 151 | 152 | f.render_stateful_widget(items, chunks[0], &mut app.boards.state); 153 | 154 | let thread_len = app.threads.items.len(); 155 | let threads: Vec = app 156 | .threads 157 | .items 158 | .iter() 159 | .enumerate() 160 | .map(|(i, thread)| { 161 | format_post_short( 162 | thread.posts().first().unwrap(), 163 | i + 1, 164 | thread_len, 165 | chunks[1], 166 | ) 167 | }) 168 | .collect(); 169 | 170 | let threads = List::new(threads) 171 | .block( 172 | Block::default() 173 | .borders(Borders::ALL) 174 | .border_style(Style::default().fg(block_style.border_color().thread_list())) 175 | .border_type(block_style.border_type().thread_list()) 176 | .title(format_default(&format!( 177 | "Threads, page {} {}", 178 | thread_list.cur_page(), 179 | thread_list.description(), 180 | ))), 181 | ) 182 | .highlight_style(Style::default().bg(*style_prov.highlight_color())); 183 | 184 | f.render_stateful_widget(threads, chunks[1], &mut app.threads.state); 185 | 186 | let thread: Vec = app 187 | .thread 188 | .items 189 | .iter() 190 | .enumerate() 191 | .map(|(i, post)| format_post_full(post, i + 1, chunks[2])) 192 | .collect(); 193 | 194 | let thread = List::new(thread) 195 | .block( 196 | Block::default() 197 | .borders(Borders::ALL) 198 | .border_style(Style::default().fg(block_style.border_color().thread())) 199 | .border_type(block_style.border_type().thread()) 200 | .title(format_default(&format!( 201 | "Thread {}", 202 | app.selected_thread_description() 203 | ))), 204 | ) 205 | .highlight_style(Style::default().bg(*style_prov.highlight_color())); 206 | f.render_stateful_widget(thread, chunks[2], &mut app.thread.state); 207 | })?; 208 | 209 | match events.next().unwrap() { 210 | Event::Input(input) => match input { 211 | _ if input == keybinds.quit => { 212 | break; 213 | } 214 | _ if input == keybinds.left => { 215 | match selected_field { 216 | SelectedField::BoardList => {} 217 | SelectedField::ThreadList => { 218 | app.set_shown_board_list(true); 219 | app.set_shown_thread(false); 220 | selected_field = SelectedField::BoardList; 221 | } 222 | SelectedField::Thread => { 223 | app.set_shown_board_list(true); 224 | app.set_shown_thread_list(true); 225 | app.set_shown_thread(false); 226 | selected_field = SelectedField::ThreadList; 227 | } 228 | }; 229 | } 230 | _ if input == keybinds.down => { 231 | const STEPS: isize = 1; 232 | app.advance(&selected_field, STEPS); 233 | } 234 | _ if input == keybinds.up => { 235 | const STEPS: isize = -1; 236 | app.advance(&selected_field, STEPS); 237 | } 238 | _ if input == keybinds.quick_down => { 239 | const STEPS: isize = 5; 240 | app.advance(&selected_field, STEPS); 241 | } 242 | _ if input == keybinds.quick_up => { 243 | const STEPS: isize = -5; 244 | app.advance(&selected_field, STEPS); 245 | } 246 | _ if input == keybinds.fullscreen => { 247 | match selected_field { 248 | SelectedField::BoardList => { 249 | if app.shown_thread_list() { 250 | app.toggle_shown_board_list(); 251 | selected_field = SelectedField::ThreadList; 252 | } 253 | } 254 | SelectedField::ThreadList => { 255 | if app.shown_thread() { 256 | app.toggle_shown_thread_list(); 257 | selected_field = SelectedField::Thread; 258 | } else { 259 | app.toggle_shown_board_list(); 260 | selected_field = SelectedField::ThreadList; 261 | } 262 | } 263 | SelectedField::Thread => { 264 | app.toggle_shown_thread_list(); 265 | selected_field = SelectedField::Thread; 266 | } 267 | }; 268 | } 269 | _ if input == keybinds.help => { 270 | app.help_bar_mut().toggle_shown(); 271 | } 272 | _ if input == keybinds.open_thread => { 273 | let url = match selected_field { 274 | SelectedField::BoardList => app.url_boards(api), 275 | SelectedField::ThreadList => app.url_threads(api), 276 | SelectedField::Thread => app.url_thread(api), 277 | }; 278 | 279 | open_in_browser(url).expect("Browser error."); 280 | } 281 | _ if input == keybinds.open_media => { 282 | let url = match selected_field { 283 | SelectedField::BoardList => None, 284 | SelectedField::ThreadList => app.media_url_threads(api), 285 | SelectedField::Thread => app.media_url_thread(api), 286 | }; 287 | 288 | if let Some(url) = url { 289 | open_in_browser(url).expect("Browser error."); 290 | } 291 | } 292 | _ if input == keybinds.copy_thread => { 293 | let url = match selected_field { 294 | SelectedField::BoardList => app.url_boards(api), 295 | SelectedField::ThreadList => app.url_threads(api), 296 | SelectedField::Thread => app.url_thread(api), 297 | }; 298 | 299 | ctx.set_contents(url).expect("Clipboard error."); 300 | } 301 | _ if input == keybinds.copy_media => { 302 | let url = match selected_field { 303 | SelectedField::BoardList => None, 304 | SelectedField::ThreadList => app.media_url_threads(api), 305 | SelectedField::Thread => app.media_url_thread(api), 306 | }; 307 | 308 | if let Some(url) = url { 309 | ctx.set_contents(url).expect("Clipboard error."); 310 | } 311 | } 312 | _ if input == keybinds.page_next => { 313 | match selected_field { 314 | SelectedField::ThreadList => { 315 | let mut threads: Vec = vec![]; 316 | runtime.block_on(async { 317 | let result = client 318 | .get_threads( 319 | app.selected_board().board(), 320 | thread_list.next_page(app.selected_board()), 321 | ) 322 | .await; 323 | match result { 324 | Ok(data) => threads = data, 325 | Err(err) => eprintln!("{:#?}", err), 326 | }; 327 | 328 | app.fill_threads(threads); 329 | }); 330 | } 331 | _ => {} 332 | }; 333 | } 334 | _ if input == keybinds.page_previous => { 335 | match selected_field { 336 | SelectedField::ThreadList => { 337 | let mut threads: Vec = vec![]; 338 | runtime.block_on(async { 339 | let result = client 340 | .get_threads( 341 | app.selected_board().board(), 342 | thread_list.prev_page(app.selected_board()), 343 | ) 344 | .await; 345 | match result { 346 | Ok(data) => threads = data, 347 | Err(err) => eprintln!("{:#?}", err), 348 | }; 349 | 350 | app.fill_threads(threads); 351 | }); 352 | } 353 | _ => {} 354 | }; 355 | } 356 | _ if input == keybinds.reload => { 357 | match selected_field { 358 | SelectedField::ThreadList => { 359 | let mut threads: Vec = vec![]; 360 | runtime.block_on(async { 361 | let result = client 362 | .get_threads( 363 | app.selected_board().board(), 364 | thread_list.cur_page(), 365 | ) 366 | .await; 367 | match result { 368 | Ok(data) => threads = data, 369 | Err(err) => eprintln!("{:#?}", err), 370 | }; 371 | 372 | app.fill_threads(threads); 373 | app.threads.advance_by(1); 374 | }); 375 | } 376 | SelectedField::Thread => { 377 | let mut thread: Vec = vec![]; 378 | runtime.block_on(async { 379 | let result = client 380 | .get_thread( 381 | app.selected_board().board(), 382 | app.selected_thread().posts().first().unwrap().no() as u64, 383 | ) 384 | .await; 385 | match result { 386 | Ok(data) => thread = data, 387 | Err(err) => eprintln!("{:#?}", err), 388 | }; 389 | 390 | app.fill_thread(thread); 391 | app.thread.advance_by(1); 392 | }); 393 | } 394 | _ => {} 395 | }; 396 | } 397 | _ if input == keybinds.right => { 398 | match selected_field { 399 | SelectedField::BoardList => { 400 | selected_field = SelectedField::ThreadList; 401 | app.set_shown_thread_list(true); 402 | 403 | thread_list = ThreadList::new(); 404 | thread_list.set_description(app.selected_board().meta_description()); 405 | let mut threads: Vec = vec![]; 406 | runtime.block_on(async { 407 | let result = client 408 | .get_threads( 409 | app.selected_board().board(), 410 | thread_list.cur_page(), 411 | ) 412 | .await; 413 | match result { 414 | Ok(data) => threads = data, 415 | Err(err) => eprintln!("{:#?}", err), 416 | }; 417 | 418 | app.fill_threads(threads); 419 | app.threads.advance_by(1); 420 | }); 421 | } 422 | SelectedField::ThreadList => { 423 | selected_field = SelectedField::Thread; 424 | app.set_shown_thread(true); 425 | app.set_shown_board_list(false); 426 | 427 | let mut thread: Vec = vec![]; 428 | runtime.block_on(async { 429 | let result = client 430 | .get_thread( 431 | app.selected_board().board(), 432 | app.selected_thread().posts().first().unwrap().no() as u64, 433 | ) 434 | .await; 435 | match result { 436 | Ok(data) => thread = data, 437 | Err(err) => eprintln!("{:#?}", err), 438 | }; 439 | 440 | app.fill_thread(thread); 441 | app.thread.advance_by(1); 442 | }); 443 | } 444 | _ => {} 445 | }; 446 | } 447 | _ => {} 448 | }, 449 | Event::Tick => { 450 | app.advance_idly(); 451 | } 452 | } 453 | } 454 | 455 | Ok(()) 456 | } 457 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "atomic-waker" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 49 | 50 | [[package]] 51 | name = "autocfg" 52 | version = "1.3.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 55 | 56 | [[package]] 57 | name = "backtrace" 58 | version = "0.3.73" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 61 | dependencies = [ 62 | "addr2line", 63 | "cc", 64 | "cfg-if", 65 | "libc", 66 | "miniz_oxide", 67 | "object", 68 | "rustc-demangle", 69 | ] 70 | 71 | [[package]] 72 | name = "base64" 73 | version = "0.22.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 76 | 77 | [[package]] 78 | name = "bitflags" 79 | version = "1.3.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 82 | 83 | [[package]] 84 | name = "bitflags" 85 | version = "2.6.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 88 | 89 | [[package]] 90 | name = "block" 91 | version = "0.1.6" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 94 | 95 | [[package]] 96 | name = "bumpalo" 97 | version = "3.16.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 100 | 101 | [[package]] 102 | name = "bytes" 103 | version = "1.7.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 106 | 107 | [[package]] 108 | name = "cassowary" 109 | version = "0.3.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 112 | 113 | [[package]] 114 | name = "cc" 115 | version = "1.1.15" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" 118 | dependencies = [ 119 | "shlex", 120 | ] 121 | 122 | [[package]] 123 | name = "cfg-if" 124 | version = "1.0.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 127 | 128 | [[package]] 129 | name = "chrono" 130 | version = "0.4.38" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 133 | dependencies = [ 134 | "android-tzdata", 135 | "iana-time-zone", 136 | "js-sys", 137 | "num-traits", 138 | "wasm-bindgen", 139 | "windows-targets", 140 | ] 141 | 142 | [[package]] 143 | name = "clipboard" 144 | version = "0.5.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" 147 | dependencies = [ 148 | "clipboard-win", 149 | "objc", 150 | "objc-foundation", 151 | "objc_id", 152 | "x11-clipboard", 153 | ] 154 | 155 | [[package]] 156 | name = "clipboard-win" 157 | version = "2.2.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" 160 | dependencies = [ 161 | "winapi", 162 | ] 163 | 164 | [[package]] 165 | name = "core-foundation" 166 | version = "0.9.4" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 169 | dependencies = [ 170 | "core-foundation-sys", 171 | "libc", 172 | ] 173 | 174 | [[package]] 175 | name = "core-foundation-sys" 176 | version = "0.8.7" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 179 | 180 | [[package]] 181 | name = "encoding_rs" 182 | version = "0.8.34" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 185 | dependencies = [ 186 | "cfg-if", 187 | ] 188 | 189 | [[package]] 190 | name = "equivalent" 191 | version = "1.0.1" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 194 | 195 | [[package]] 196 | name = "errno" 197 | version = "0.3.9" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 200 | dependencies = [ 201 | "libc", 202 | "windows-sys 0.52.0", 203 | ] 204 | 205 | [[package]] 206 | name = "fastrand" 207 | version = "2.1.1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 210 | 211 | [[package]] 212 | name = "fnv" 213 | version = "1.0.7" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 216 | 217 | [[package]] 218 | name = "foreign-types" 219 | version = "0.3.2" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 222 | dependencies = [ 223 | "foreign-types-shared", 224 | ] 225 | 226 | [[package]] 227 | name = "foreign-types-shared" 228 | version = "0.1.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 231 | 232 | [[package]] 233 | name = "form_urlencoded" 234 | version = "1.2.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 237 | dependencies = [ 238 | "percent-encoding", 239 | ] 240 | 241 | [[package]] 242 | name = "futures-channel" 243 | version = "0.3.30" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 246 | dependencies = [ 247 | "futures-core", 248 | ] 249 | 250 | [[package]] 251 | name = "futures-core" 252 | version = "0.3.30" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 255 | 256 | [[package]] 257 | name = "futures-sink" 258 | version = "0.3.30" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 261 | 262 | [[package]] 263 | name = "futures-task" 264 | version = "0.3.30" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 267 | 268 | [[package]] 269 | name = "futures-util" 270 | version = "0.3.30" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 273 | dependencies = [ 274 | "futures-core", 275 | "futures-task", 276 | "pin-project-lite", 277 | "pin-utils", 278 | ] 279 | 280 | [[package]] 281 | name = "getrandom" 282 | version = "0.2.15" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 285 | dependencies = [ 286 | "cfg-if", 287 | "libc", 288 | "wasi", 289 | ] 290 | 291 | [[package]] 292 | name = "gimli" 293 | version = "0.29.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 296 | 297 | [[package]] 298 | name = "h2" 299 | version = "0.4.6" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" 302 | dependencies = [ 303 | "atomic-waker", 304 | "bytes", 305 | "fnv", 306 | "futures-core", 307 | "futures-sink", 308 | "http", 309 | "indexmap", 310 | "slab", 311 | "tokio", 312 | "tokio-util", 313 | "tracing", 314 | ] 315 | 316 | [[package]] 317 | name = "hashbrown" 318 | version = "0.14.5" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 321 | 322 | [[package]] 323 | name = "hermit-abi" 324 | version = "0.3.9" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 327 | 328 | [[package]] 329 | name = "htmlescape" 330 | version = "0.3.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" 333 | 334 | [[package]] 335 | name = "http" 336 | version = "1.1.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 339 | dependencies = [ 340 | "bytes", 341 | "fnv", 342 | "itoa", 343 | ] 344 | 345 | [[package]] 346 | name = "http-body" 347 | version = "1.0.1" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 350 | dependencies = [ 351 | "bytes", 352 | "http", 353 | ] 354 | 355 | [[package]] 356 | name = "http-body-util" 357 | version = "0.1.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 360 | dependencies = [ 361 | "bytes", 362 | "futures-util", 363 | "http", 364 | "http-body", 365 | "pin-project-lite", 366 | ] 367 | 368 | [[package]] 369 | name = "httparse" 370 | version = "1.9.4" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 373 | 374 | [[package]] 375 | name = "hyper" 376 | version = "1.4.1" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 379 | dependencies = [ 380 | "bytes", 381 | "futures-channel", 382 | "futures-util", 383 | "h2", 384 | "http", 385 | "http-body", 386 | "httparse", 387 | "itoa", 388 | "pin-project-lite", 389 | "smallvec", 390 | "tokio", 391 | "want", 392 | ] 393 | 394 | [[package]] 395 | name = "hyper-rustls" 396 | version = "0.27.2" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" 399 | dependencies = [ 400 | "futures-util", 401 | "http", 402 | "hyper", 403 | "hyper-util", 404 | "rustls", 405 | "rustls-pki-types", 406 | "tokio", 407 | "tokio-rustls", 408 | "tower-service", 409 | ] 410 | 411 | [[package]] 412 | name = "hyper-tls" 413 | version = "0.6.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 416 | dependencies = [ 417 | "bytes", 418 | "http-body-util", 419 | "hyper", 420 | "hyper-util", 421 | "native-tls", 422 | "tokio", 423 | "tokio-native-tls", 424 | "tower-service", 425 | ] 426 | 427 | [[package]] 428 | name = "hyper-util" 429 | version = "0.1.7" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" 432 | dependencies = [ 433 | "bytes", 434 | "futures-channel", 435 | "futures-util", 436 | "http", 437 | "http-body", 438 | "hyper", 439 | "pin-project-lite", 440 | "socket2", 441 | "tokio", 442 | "tower", 443 | "tower-service", 444 | "tracing", 445 | ] 446 | 447 | [[package]] 448 | name = "iana-time-zone" 449 | version = "0.1.60" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 452 | dependencies = [ 453 | "android_system_properties", 454 | "core-foundation-sys", 455 | "iana-time-zone-haiku", 456 | "js-sys", 457 | "wasm-bindgen", 458 | "windows-core", 459 | ] 460 | 461 | [[package]] 462 | name = "iana-time-zone-haiku" 463 | version = "0.1.2" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 466 | dependencies = [ 467 | "cc", 468 | ] 469 | 470 | [[package]] 471 | name = "idna" 472 | version = "0.5.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 475 | dependencies = [ 476 | "unicode-bidi", 477 | "unicode-normalization", 478 | ] 479 | 480 | [[package]] 481 | name = "indexmap" 482 | version = "2.5.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" 485 | dependencies = [ 486 | "equivalent", 487 | "hashbrown", 488 | ] 489 | 490 | [[package]] 491 | name = "ipnet" 492 | version = "2.9.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 495 | 496 | [[package]] 497 | name = "itoa" 498 | version = "1.0.11" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 501 | 502 | [[package]] 503 | name = "js-sys" 504 | version = "0.3.70" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" 507 | dependencies = [ 508 | "wasm-bindgen", 509 | ] 510 | 511 | [[package]] 512 | name = "libc" 513 | version = "0.2.158" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 516 | 517 | [[package]] 518 | name = "linux-raw-sys" 519 | version = "0.4.14" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 522 | 523 | [[package]] 524 | name = "lock_api" 525 | version = "0.4.12" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 528 | dependencies = [ 529 | "autocfg", 530 | "scopeguard", 531 | ] 532 | 533 | [[package]] 534 | name = "log" 535 | version = "0.4.22" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 538 | 539 | [[package]] 540 | name = "malloc_buf" 541 | version = "0.0.6" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 544 | dependencies = [ 545 | "libc", 546 | ] 547 | 548 | [[package]] 549 | name = "memchr" 550 | version = "2.7.4" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 553 | 554 | [[package]] 555 | name = "mime" 556 | version = "0.3.17" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 559 | 560 | [[package]] 561 | name = "miniz_oxide" 562 | version = "0.7.4" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 565 | dependencies = [ 566 | "adler", 567 | ] 568 | 569 | [[package]] 570 | name = "mio" 571 | version = "1.0.2" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 574 | dependencies = [ 575 | "hermit-abi", 576 | "libc", 577 | "wasi", 578 | "windows-sys 0.52.0", 579 | ] 580 | 581 | [[package]] 582 | name = "native-tls" 583 | version = "0.2.12" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 586 | dependencies = [ 587 | "libc", 588 | "log", 589 | "openssl", 590 | "openssl-probe", 591 | "openssl-sys", 592 | "schannel", 593 | "security-framework", 594 | "security-framework-sys", 595 | "tempfile", 596 | ] 597 | 598 | [[package]] 599 | name = "num-traits" 600 | version = "0.2.19" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 603 | dependencies = [ 604 | "autocfg", 605 | ] 606 | 607 | [[package]] 608 | name = "numtoa" 609 | version = "0.1.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 612 | 613 | [[package]] 614 | name = "objc" 615 | version = "0.2.7" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 618 | dependencies = [ 619 | "malloc_buf", 620 | ] 621 | 622 | [[package]] 623 | name = "objc-foundation" 624 | version = "0.1.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 627 | dependencies = [ 628 | "block", 629 | "objc", 630 | "objc_id", 631 | ] 632 | 633 | [[package]] 634 | name = "objc_id" 635 | version = "0.1.1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 638 | dependencies = [ 639 | "objc", 640 | ] 641 | 642 | [[package]] 643 | name = "object" 644 | version = "0.36.4" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 647 | dependencies = [ 648 | "memchr", 649 | ] 650 | 651 | [[package]] 652 | name = "once_cell" 653 | version = "1.19.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 656 | 657 | [[package]] 658 | name = "open" 659 | version = "1.7.1" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "dcea7a30d6b81a2423cc59c43554880feff7b57d12916f231a79f8d6d9470201" 662 | dependencies = [ 663 | "pathdiff", 664 | "winapi", 665 | ] 666 | 667 | [[package]] 668 | name = "openssl" 669 | version = "0.10.66" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" 672 | dependencies = [ 673 | "bitflags 2.6.0", 674 | "cfg-if", 675 | "foreign-types", 676 | "libc", 677 | "once_cell", 678 | "openssl-macros", 679 | "openssl-sys", 680 | ] 681 | 682 | [[package]] 683 | name = "openssl-macros" 684 | version = "0.1.1" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 687 | dependencies = [ 688 | "proc-macro2", 689 | "quote", 690 | "syn", 691 | ] 692 | 693 | [[package]] 694 | name = "openssl-probe" 695 | version = "0.1.5" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 698 | 699 | [[package]] 700 | name = "openssl-sys" 701 | version = "0.9.103" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 704 | dependencies = [ 705 | "cc", 706 | "libc", 707 | "pkg-config", 708 | "vcpkg", 709 | ] 710 | 711 | [[package]] 712 | name = "parking_lot" 713 | version = "0.12.3" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 716 | dependencies = [ 717 | "lock_api", 718 | "parking_lot_core", 719 | ] 720 | 721 | [[package]] 722 | name = "parking_lot_core" 723 | version = "0.9.10" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 726 | dependencies = [ 727 | "cfg-if", 728 | "libc", 729 | "redox_syscall 0.5.3", 730 | "smallvec", 731 | "windows-targets", 732 | ] 733 | 734 | [[package]] 735 | name = "pathdiff" 736 | version = "0.2.1" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 739 | 740 | [[package]] 741 | name = "percent-encoding" 742 | version = "2.3.1" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 745 | 746 | [[package]] 747 | name = "pin-project" 748 | version = "1.1.5" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 751 | dependencies = [ 752 | "pin-project-internal", 753 | ] 754 | 755 | [[package]] 756 | name = "pin-project-internal" 757 | version = "1.1.5" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 760 | dependencies = [ 761 | "proc-macro2", 762 | "quote", 763 | "syn", 764 | ] 765 | 766 | [[package]] 767 | name = "pin-project-lite" 768 | version = "0.2.14" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 771 | 772 | [[package]] 773 | name = "pin-utils" 774 | version = "0.1.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 777 | 778 | [[package]] 779 | name = "pkg-config" 780 | version = "0.3.30" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 783 | 784 | [[package]] 785 | name = "proc-macro2" 786 | version = "1.0.86" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 789 | dependencies = [ 790 | "unicode-ident", 791 | ] 792 | 793 | [[package]] 794 | name = "quote" 795 | version = "1.0.37" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 798 | dependencies = [ 799 | "proc-macro2", 800 | ] 801 | 802 | [[package]] 803 | name = "redox_syscall" 804 | version = "0.2.16" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 807 | dependencies = [ 808 | "bitflags 1.3.2", 809 | ] 810 | 811 | [[package]] 812 | name = "redox_syscall" 813 | version = "0.5.3" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 816 | dependencies = [ 817 | "bitflags 2.6.0", 818 | ] 819 | 820 | [[package]] 821 | name = "redox_termios" 822 | version = "0.1.3" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" 825 | 826 | [[package]] 827 | name = "regex" 828 | version = "1.10.6" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 831 | dependencies = [ 832 | "aho-corasick", 833 | "memchr", 834 | "regex-automata", 835 | "regex-syntax", 836 | ] 837 | 838 | [[package]] 839 | name = "regex-automata" 840 | version = "0.4.7" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 843 | dependencies = [ 844 | "aho-corasick", 845 | "memchr", 846 | "regex-syntax", 847 | ] 848 | 849 | [[package]] 850 | name = "regex-syntax" 851 | version = "0.8.4" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 854 | 855 | [[package]] 856 | name = "reqwest" 857 | version = "0.12.7" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" 860 | dependencies = [ 861 | "base64", 862 | "bytes", 863 | "encoding_rs", 864 | "futures-core", 865 | "futures-util", 866 | "h2", 867 | "http", 868 | "http-body", 869 | "http-body-util", 870 | "hyper", 871 | "hyper-rustls", 872 | "hyper-tls", 873 | "hyper-util", 874 | "ipnet", 875 | "js-sys", 876 | "log", 877 | "mime", 878 | "native-tls", 879 | "once_cell", 880 | "percent-encoding", 881 | "pin-project-lite", 882 | "rustls-pemfile", 883 | "serde", 884 | "serde_json", 885 | "serde_urlencoded", 886 | "sync_wrapper", 887 | "system-configuration", 888 | "tokio", 889 | "tokio-native-tls", 890 | "tower-service", 891 | "url", 892 | "wasm-bindgen", 893 | "wasm-bindgen-futures", 894 | "web-sys", 895 | "windows-registry", 896 | ] 897 | 898 | [[package]] 899 | name = "ring" 900 | version = "0.17.8" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 903 | dependencies = [ 904 | "cc", 905 | "cfg-if", 906 | "getrandom", 907 | "libc", 908 | "spin", 909 | "untrusted", 910 | "windows-sys 0.52.0", 911 | ] 912 | 913 | [[package]] 914 | name = "rustc-demangle" 915 | version = "0.1.24" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 918 | 919 | [[package]] 920 | name = "rustix" 921 | version = "0.38.35" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" 924 | dependencies = [ 925 | "bitflags 2.6.0", 926 | "errno", 927 | "libc", 928 | "linux-raw-sys", 929 | "windows-sys 0.52.0", 930 | ] 931 | 932 | [[package]] 933 | name = "rustls" 934 | version = "0.23.12" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" 937 | dependencies = [ 938 | "once_cell", 939 | "rustls-pki-types", 940 | "rustls-webpki", 941 | "subtle", 942 | "zeroize", 943 | ] 944 | 945 | [[package]] 946 | name = "rustls-pemfile" 947 | version = "2.1.3" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" 950 | dependencies = [ 951 | "base64", 952 | "rustls-pki-types", 953 | ] 954 | 955 | [[package]] 956 | name = "rustls-pki-types" 957 | version = "1.8.0" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" 960 | 961 | [[package]] 962 | name = "rustls-webpki" 963 | version = "0.102.7" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" 966 | dependencies = [ 967 | "ring", 968 | "rustls-pki-types", 969 | "untrusted", 970 | ] 971 | 972 | [[package]] 973 | name = "ryu" 974 | version = "1.0.18" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 977 | 978 | [[package]] 979 | name = "schannel" 980 | version = "0.1.23" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 983 | dependencies = [ 984 | "windows-sys 0.52.0", 985 | ] 986 | 987 | [[package]] 988 | name = "scopeguard" 989 | version = "1.2.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 992 | 993 | [[package]] 994 | name = "security-framework" 995 | version = "2.11.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 998 | dependencies = [ 999 | "bitflags 2.6.0", 1000 | "core-foundation", 1001 | "core-foundation-sys", 1002 | "libc", 1003 | "security-framework-sys", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "security-framework-sys" 1008 | version = "2.11.1" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" 1011 | dependencies = [ 1012 | "core-foundation-sys", 1013 | "libc", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "serde" 1018 | version = "1.0.209" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" 1021 | dependencies = [ 1022 | "serde_derive", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "serde_derive" 1027 | version = "1.0.209" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" 1030 | dependencies = [ 1031 | "proc-macro2", 1032 | "quote", 1033 | "syn", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "serde_json" 1038 | version = "1.0.127" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" 1041 | dependencies = [ 1042 | "itoa", 1043 | "memchr", 1044 | "ryu", 1045 | "serde", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "serde_urlencoded" 1050 | version = "0.7.1" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1053 | dependencies = [ 1054 | "form_urlencoded", 1055 | "itoa", 1056 | "ryu", 1057 | "serde", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "shlex" 1062 | version = "1.3.0" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1065 | 1066 | [[package]] 1067 | name = "signal-hook-registry" 1068 | version = "1.4.2" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1071 | dependencies = [ 1072 | "libc", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "slab" 1077 | version = "0.4.9" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1080 | dependencies = [ 1081 | "autocfg", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "smallvec" 1086 | version = "1.13.2" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1089 | 1090 | [[package]] 1091 | name = "socket2" 1092 | version = "0.5.7" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1095 | dependencies = [ 1096 | "libc", 1097 | "windows-sys 0.52.0", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "spin" 1102 | version = "0.9.8" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1105 | 1106 | [[package]] 1107 | name = "stfu8" 1108 | version = "0.2.7" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" 1111 | 1112 | [[package]] 1113 | name = "subtle" 1114 | version = "2.6.1" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1117 | 1118 | [[package]] 1119 | name = "syn" 1120 | version = "2.0.77" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 1123 | dependencies = [ 1124 | "proc-macro2", 1125 | "quote", 1126 | "unicode-ident", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "sync_wrapper" 1131 | version = "1.0.1" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1134 | dependencies = [ 1135 | "futures-core", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "system-configuration" 1140 | version = "0.6.1" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1143 | dependencies = [ 1144 | "bitflags 2.6.0", 1145 | "core-foundation", 1146 | "system-configuration-sys", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "system-configuration-sys" 1151 | version = "0.6.0" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1154 | dependencies = [ 1155 | "core-foundation-sys", 1156 | "libc", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "tempfile" 1161 | version = "3.12.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" 1164 | dependencies = [ 1165 | "cfg-if", 1166 | "fastrand", 1167 | "once_cell", 1168 | "rustix", 1169 | "windows-sys 0.59.0", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "termion" 1174 | version = "1.5.6" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" 1177 | dependencies = [ 1178 | "libc", 1179 | "numtoa", 1180 | "redox_syscall 0.2.16", 1181 | "redox_termios", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "tinyvec" 1186 | version = "1.8.0" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1189 | dependencies = [ 1190 | "tinyvec_macros", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "tinyvec_macros" 1195 | version = "0.1.1" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1198 | 1199 | [[package]] 1200 | name = "tokio" 1201 | version = "1.40.0" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 1204 | dependencies = [ 1205 | "backtrace", 1206 | "bytes", 1207 | "libc", 1208 | "mio", 1209 | "parking_lot", 1210 | "pin-project-lite", 1211 | "signal-hook-registry", 1212 | "socket2", 1213 | "tokio-macros", 1214 | "windows-sys 0.52.0", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "tokio-macros" 1219 | version = "2.4.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 1222 | dependencies = [ 1223 | "proc-macro2", 1224 | "quote", 1225 | "syn", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "tokio-native-tls" 1230 | version = "0.3.1" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1233 | dependencies = [ 1234 | "native-tls", 1235 | "tokio", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "tokio-rustls" 1240 | version = "0.26.0" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 1243 | dependencies = [ 1244 | "rustls", 1245 | "rustls-pki-types", 1246 | "tokio", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "tokio-util" 1251 | version = "0.7.11" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 1254 | dependencies = [ 1255 | "bytes", 1256 | "futures-core", 1257 | "futures-sink", 1258 | "pin-project-lite", 1259 | "tokio", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "tower" 1264 | version = "0.4.13" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1267 | dependencies = [ 1268 | "futures-core", 1269 | "futures-util", 1270 | "pin-project", 1271 | "pin-project-lite", 1272 | "tokio", 1273 | "tower-layer", 1274 | "tower-service", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "tower-layer" 1279 | version = "0.3.3" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1282 | 1283 | [[package]] 1284 | name = "tower-service" 1285 | version = "0.3.3" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1288 | 1289 | [[package]] 1290 | name = "tracing" 1291 | version = "0.1.40" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1294 | dependencies = [ 1295 | "pin-project-lite", 1296 | "tracing-core", 1297 | ] 1298 | 1299 | [[package]] 1300 | name = "tracing-core" 1301 | version = "0.1.32" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1304 | dependencies = [ 1305 | "once_cell", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "try-lock" 1310 | version = "0.2.5" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1313 | 1314 | [[package]] 1315 | name = "tui" 1316 | version = "0.16.0" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" 1319 | dependencies = [ 1320 | "bitflags 1.3.2", 1321 | "cassowary", 1322 | "termion", 1323 | "unicode-segmentation", 1324 | "unicode-width", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "tui-chan" 1329 | version = "0.5.0" 1330 | dependencies = [ 1331 | "chrono", 1332 | "clipboard", 1333 | "htmlescape", 1334 | "open", 1335 | "regex", 1336 | "reqwest", 1337 | "serde", 1338 | "serde_json", 1339 | "termion", 1340 | "tokio", 1341 | "tui", 1342 | "voca_rs", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "unicode-bidi" 1347 | version = "0.3.15" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1350 | 1351 | [[package]] 1352 | name = "unicode-ident" 1353 | version = "1.0.12" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1356 | 1357 | [[package]] 1358 | name = "unicode-normalization" 1359 | version = "0.1.23" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1362 | dependencies = [ 1363 | "tinyvec", 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "unicode-segmentation" 1368 | version = "1.11.0" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1371 | 1372 | [[package]] 1373 | name = "unicode-width" 1374 | version = "0.1.13" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 1377 | 1378 | [[package]] 1379 | name = "untrusted" 1380 | version = "0.9.0" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1383 | 1384 | [[package]] 1385 | name = "url" 1386 | version = "2.5.2" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1389 | dependencies = [ 1390 | "form_urlencoded", 1391 | "idna", 1392 | "percent-encoding", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "vcpkg" 1397 | version = "0.2.15" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1400 | 1401 | [[package]] 1402 | name = "voca_rs" 1403 | version = "1.15.2" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "3e44efbf25e32768d5ecd22244feacc3d3b3eca72d318f5ef0a4764c2c158e18" 1406 | dependencies = [ 1407 | "regex", 1408 | "stfu8", 1409 | "unicode-segmentation", 1410 | ] 1411 | 1412 | [[package]] 1413 | name = "want" 1414 | version = "0.3.1" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1417 | dependencies = [ 1418 | "try-lock", 1419 | ] 1420 | 1421 | [[package]] 1422 | name = "wasi" 1423 | version = "0.11.0+wasi-snapshot-preview1" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1426 | 1427 | [[package]] 1428 | name = "wasm-bindgen" 1429 | version = "0.2.93" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 1432 | dependencies = [ 1433 | "cfg-if", 1434 | "once_cell", 1435 | "wasm-bindgen-macro", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "wasm-bindgen-backend" 1440 | version = "0.2.93" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 1443 | dependencies = [ 1444 | "bumpalo", 1445 | "log", 1446 | "once_cell", 1447 | "proc-macro2", 1448 | "quote", 1449 | "syn", 1450 | "wasm-bindgen-shared", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "wasm-bindgen-futures" 1455 | version = "0.4.43" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" 1458 | dependencies = [ 1459 | "cfg-if", 1460 | "js-sys", 1461 | "wasm-bindgen", 1462 | "web-sys", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "wasm-bindgen-macro" 1467 | version = "0.2.93" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 1470 | dependencies = [ 1471 | "quote", 1472 | "wasm-bindgen-macro-support", 1473 | ] 1474 | 1475 | [[package]] 1476 | name = "wasm-bindgen-macro-support" 1477 | version = "0.2.93" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 1480 | dependencies = [ 1481 | "proc-macro2", 1482 | "quote", 1483 | "syn", 1484 | "wasm-bindgen-backend", 1485 | "wasm-bindgen-shared", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "wasm-bindgen-shared" 1490 | version = "0.2.93" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 1493 | 1494 | [[package]] 1495 | name = "web-sys" 1496 | version = "0.3.70" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" 1499 | dependencies = [ 1500 | "js-sys", 1501 | "wasm-bindgen", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "winapi" 1506 | version = "0.3.9" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1509 | dependencies = [ 1510 | "winapi-i686-pc-windows-gnu", 1511 | "winapi-x86_64-pc-windows-gnu", 1512 | ] 1513 | 1514 | [[package]] 1515 | name = "winapi-i686-pc-windows-gnu" 1516 | version = "0.4.0" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1519 | 1520 | [[package]] 1521 | name = "winapi-x86_64-pc-windows-gnu" 1522 | version = "0.4.0" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1525 | 1526 | [[package]] 1527 | name = "windows-core" 1528 | version = "0.52.0" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1531 | dependencies = [ 1532 | "windows-targets", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "windows-registry" 1537 | version = "0.2.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 1540 | dependencies = [ 1541 | "windows-result", 1542 | "windows-strings", 1543 | "windows-targets", 1544 | ] 1545 | 1546 | [[package]] 1547 | name = "windows-result" 1548 | version = "0.2.0" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 1551 | dependencies = [ 1552 | "windows-targets", 1553 | ] 1554 | 1555 | [[package]] 1556 | name = "windows-strings" 1557 | version = "0.1.0" 1558 | source = "registry+https://github.com/rust-lang/crates.io-index" 1559 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 1560 | dependencies = [ 1561 | "windows-result", 1562 | "windows-targets", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "windows-sys" 1567 | version = "0.52.0" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1570 | dependencies = [ 1571 | "windows-targets", 1572 | ] 1573 | 1574 | [[package]] 1575 | name = "windows-sys" 1576 | version = "0.59.0" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1579 | dependencies = [ 1580 | "windows-targets", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "windows-targets" 1585 | version = "0.52.6" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1588 | dependencies = [ 1589 | "windows_aarch64_gnullvm", 1590 | "windows_aarch64_msvc", 1591 | "windows_i686_gnu", 1592 | "windows_i686_gnullvm", 1593 | "windows_i686_msvc", 1594 | "windows_x86_64_gnu", 1595 | "windows_x86_64_gnullvm", 1596 | "windows_x86_64_msvc", 1597 | ] 1598 | 1599 | [[package]] 1600 | name = "windows_aarch64_gnullvm" 1601 | version = "0.52.6" 1602 | source = "registry+https://github.com/rust-lang/crates.io-index" 1603 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1604 | 1605 | [[package]] 1606 | name = "windows_aarch64_msvc" 1607 | version = "0.52.6" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1610 | 1611 | [[package]] 1612 | name = "windows_i686_gnu" 1613 | version = "0.52.6" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1616 | 1617 | [[package]] 1618 | name = "windows_i686_gnullvm" 1619 | version = "0.52.6" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1622 | 1623 | [[package]] 1624 | name = "windows_i686_msvc" 1625 | version = "0.52.6" 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" 1627 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1628 | 1629 | [[package]] 1630 | name = "windows_x86_64_gnu" 1631 | version = "0.52.6" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1634 | 1635 | [[package]] 1636 | name = "windows_x86_64_gnullvm" 1637 | version = "0.52.6" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1640 | 1641 | [[package]] 1642 | name = "windows_x86_64_msvc" 1643 | version = "0.52.6" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1646 | 1647 | [[package]] 1648 | name = "x11-clipboard" 1649 | version = "0.3.3" 1650 | source = "registry+https://github.com/rust-lang/crates.io-index" 1651 | checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" 1652 | dependencies = [ 1653 | "xcb", 1654 | ] 1655 | 1656 | [[package]] 1657 | name = "xcb" 1658 | version = "0.8.2" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" 1661 | dependencies = [ 1662 | "libc", 1663 | "log", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "zeroize" 1668 | version = "1.8.1" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1671 | --------------------------------------------------------------------------------