├── .gitignore ├── src ├── lib.rs ├── hex.rs ├── utils.rs ├── main.rs ├── time.rs ├── input.rs ├── ui.rs └── app.rs ├── Cargo.toml ├── welcome.txt ├── examples └── window.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | mod hex; 3 | pub mod input; 4 | mod time; 5 | pub mod ui; 6 | mod utils; 7 | -------------------------------------------------------------------------------- /src/hex.rs: -------------------------------------------------------------------------------- 1 | pub fn to(addr: &[u8]) -> String { 2 | addr.iter() 3 | .map(|byte| format!["{:x}", byte]) 4 | .collect::>() 5 | .join("") 6 | } 7 | 8 | pub fn from(s: &str) -> Option> { 9 | let mut result = Vec::with_capacity((s.len() + 1) / 2); 10 | for i in 0..(s.len() + 1) / 2 { 11 | if let Ok(b) = u8::from_str_radix(&s[i * 2..=(i * 2 + 1).min(s.len())], 16) { 12 | result.push(b); 13 | } else { 14 | return None; 15 | } 16 | } 17 | Some(result) 18 | } 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cabin" 3 | version = "1.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ansi-diff = "1.0.0" 8 | argmap = "1.1.1" 9 | async-std = "1.10.0" 10 | cable = { git = "https://github.com/cabal-club/cable.rs" } 11 | cable_core = { git = "https://github.com/cabal-club/cable.rs" } 12 | chrono = { version = "0.4.30", default_features = false, features = ["alloc", "std", "clock"] } 13 | env_logger = "0.9.0" 14 | futures = "0.3.13" 15 | log = "0.4.0" 16 | owo-colors = "3.5.0" 17 | raw_tty = "0.1.0" 18 | signal-hook = { version = "0.3.13", features = [ "iterator", "extended-siginfo" ] } 19 | term_size = "0.3.2" 20 | terminal-keycode = "1.0.0" 21 | -------------------------------------------------------------------------------- /welcome.txt: -------------------------------------------------------------------------------- 1 | ( ) 2 | ( ) 3 | ( ) 4 | ( ) 5 | ( ) 6 | ( ) 7 | (_) 8 | __________________|_|_ 9 | /\ ______ \ 10 | //_\ \ /\ \ 11 | //___\ \__/ \ \ 12 | //_____\ \ |[]| \ 13 | //_______\ \|__| \ 14 | //_________\ \ 15 | //<<<<<<<<<<<\_____________________\ 16 | |_|| |__|___[ ]__|_____[ ]____| 17 | |_||o |__|___[__]__|_____[_]____| 18 | | ||__| | | | 19 | wwww/ /wwwwwwwwwwwwwwwwwwwwwwwwwww 20 | 21 | welcome to cabin! 22 | we hope you enjoy your stay. 23 | 24 | type /help for a list of commands. 25 | for more info, visit https://cabal.chat/ 26 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::AnsiColors; 2 | 3 | fn pick_colour(num: u64) -> AnsiColors { 4 | match num { 5 | 1 => AnsiColors::Red, 6 | 2 => AnsiColors::Green, 7 | 3 => AnsiColors::Yellow, 8 | 4 => AnsiColors::Blue, 9 | 5 => AnsiColors::Magenta, 10 | 6 => AnsiColors::Cyan, 11 | 7 => AnsiColors::BrightRed, 12 | 8 => AnsiColors::BrightGreen, 13 | 9 => AnsiColors::BrightYellow, 14 | 10 => AnsiColors::BrightBlue, 15 | 11 => AnsiColors::BrightMagenta, 16 | 12 => AnsiColors::BrightCyan, 17 | _ => AnsiColors::White, 18 | } 19 | } 20 | 21 | /// Pick a colour based on the sum of the base16 digits comprising 22 | /// the given public key. 23 | pub fn public_key_to_colour(public_key: &[u8; 32]) -> AnsiColors { 24 | // A return type of `u64` is used to avoid the overflow which will 25 | // likely occur if returning `u8`. 26 | let sum: u64 = public_key.iter().map(|x| *x as u64).sum(); 27 | 28 | pick_colour(sum % 12) 29 | } 30 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, io}; 2 | 3 | use async_std::task; 4 | use cable::Channel; 5 | use cable_core::MemoryStore; 6 | use futures::channel::mpsc; 7 | use raw_tty::IntoRawMode; 8 | 9 | use cabin::{app::App, ui}; 10 | 11 | type Error = Box; 12 | 13 | fn main() -> Result<(), Error> { 14 | // Initialise the logger. 15 | env_logger::init(); 16 | 17 | // Parse the arguments. 18 | let (_args, _argv) = argmap::parse(env::args()); 19 | 20 | // Launch the application, resize the UI to match the terminal dimensions 21 | // and accept input via stdin. 22 | task::block_on(async move { 23 | let (close_channel_sender, close_channel_receiver) = mpsc::unbounded::(); 24 | 25 | let mut app = App::new( 26 | ui::get_term_size(), 27 | Box::new(|_name| Box::::default()), 28 | close_channel_sender, 29 | ); 30 | 31 | let ui = app.ui.clone(); 32 | task::spawn(async move { ui::resizer(ui).await }); 33 | 34 | app.run( 35 | Box::new(io::stdin().into_raw_mode().unwrap()), 36 | close_channel_receiver, 37 | ) 38 | .await?; 39 | 40 | Ok(()) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | //! Time-related helper functions. 2 | 3 | use std::time::{SystemTime, UNIX_EPOCH}; 4 | 5 | use cable::Error; 6 | use chrono::{Local, LocalResult, TimeZone}; 7 | 8 | /// Return the current system time in seconds since the Unix epoch. 9 | pub fn now() -> Result { 10 | let now = SystemTime::now() 11 | .duration_since(UNIX_EPOCH)? 12 | .as_millis() 13 | .try_into()?; 14 | 15 | Ok(now) 16 | } 17 | 18 | /// Return the time defining two weeks before the current system time. 19 | /// 20 | /// Used to calculate the start time for channel time range 21 | /// requests. 22 | pub fn two_weeks_ago() -> Result { 23 | let two_weeks_ago = now()? - 1_209_600_000; 24 | 25 | Ok(two_weeks_ago) 26 | } 27 | 28 | /// Format the given timestamp (represented in milliseconds since the Unix 29 | /// epoch) as hour and minutes relative to the local timezone. 30 | pub fn format(timestamp: u64) -> String { 31 | if let LocalResult::Single(date_time) = Local.timestamp_millis_opt(timestamp as i64) { 32 | format!("{}", date_time.format("%H:%M")) 33 | } else { 34 | // Something is wrong with the timestamp; display a place-holder to 35 | // avoid panicking on an unwrap. 36 | String::from("XX:XX") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use terminal_keycode::{Decoder, KeyCode}; 3 | 4 | #[derive(Default)] 5 | pub struct Input { 6 | pub history: Vec, 7 | pub value: String, 8 | pub cursor: usize, 9 | decoder: Decoder, 10 | queue: VecDeque, 11 | } 12 | 13 | pub enum InputEvent { 14 | Line(String), 15 | KeyCode(KeyCode), 16 | } 17 | 18 | impl Input { 19 | pub fn putc(&mut self, b: u8) { 20 | for keycode in self.decoder.write(b) { 21 | match keycode { 22 | KeyCode::Enter | KeyCode::Linefeed => { 23 | self.queue.push_back(InputEvent::Line(self.value.clone())); 24 | self.value = String::default(); 25 | } 26 | KeyCode::Backspace | KeyCode::CtrlH => { 27 | self.remove_left(1); 28 | } 29 | KeyCode::Delete => { 30 | self.remove_right(1); 31 | } 32 | KeyCode::ArrowLeft => { 33 | self.cursor = self.cursor.max(1) - 1; 34 | } 35 | KeyCode::ArrowRight => { 36 | self.cursor = (self.cursor + 1).min(self.value.len()); 37 | } 38 | KeyCode::Home => { 39 | self.cursor = 0; 40 | } 41 | KeyCode::End => { 42 | self.cursor = self.value.len(); 43 | } 44 | code => { 45 | if let Some(c) = code.printable() { 46 | self.put_str(&c.to_string()); 47 | } else { 48 | self.queue.push_back(InputEvent::KeyCode(code)); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | pub fn next_event(&mut self) -> Option { 56 | self.queue.pop_front() 57 | } 58 | 59 | fn put_str(&mut self, s: &str) { 60 | let c = self.cursor.min(self.value.len()); 61 | self.value = self.value[0..c].to_string() + s + &self.value[c..]; 62 | self.cursor = (self.cursor + 1).min(self.value.len()); 63 | } 64 | 65 | pub fn set_value(&mut self, input: &str) { 66 | self.value = input.to_string(); 67 | self.cursor = self.cursor.min(self.value.len()); 68 | } 69 | 70 | pub fn remove_left(&mut self, n: usize) { 71 | let len = self.value.len(); 72 | let c = self.cursor; 73 | self.value = self.value[0..c.max(n) - n].to_string() + &self.value[c.min(len)..]; 74 | self.cursor = self.cursor.max(n) - n; 75 | } 76 | 77 | pub fn remove_right(&mut self, n: usize) { 78 | let len = self.value.len(); 79 | let c = self.cursor; 80 | self.value = self.value[0..c].to_string() + &self.value[(c + n).min(len)..]; 81 | } 82 | 83 | pub fn set_cursor(&mut self, cursor: usize) { 84 | self.cursor = cursor; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/window.rs: -------------------------------------------------------------------------------- 1 | //! Creates two UI windows and writes text to each. 2 | //! 3 | //! Enter `/win 1` or `/win 2` to switch between the two windows. 4 | //! 5 | //! Enter `/quit` to exit the application. 6 | 7 | use std::io::Read; 8 | 9 | use async_std::{ 10 | sync::{Arc, Mutex}, 11 | task, 12 | }; 13 | use cabin::{input::InputEvent, ui, ui::Ui}; 14 | use raw_tty::IntoRawMode; 15 | 16 | fn main() { 17 | // Create a new instance of the UI, sized to the terminal dimensions. 18 | let ui = Arc::new(Mutex::new(Ui::new(ui::get_term_size()))); 19 | 20 | let ui_clone = ui.clone(); 21 | // Resize the UI if the size of the terminal changes. 22 | task::spawn(async move { ui::resizer(ui_clone).await }); 23 | 24 | let ui_clone = ui.clone(); 25 | // Add two UI windows and write text to both. 26 | task::block_on(async move { 27 | let mut ui = ui_clone.lock().await; 28 | ui.add_window(vec![0; 32], "one".to_string()); 29 | ui.add_window(vec![1; 32], "two".to_string()); 30 | ui.write(1, "test line 1"); 31 | ui.write(1, "test line 2"); 32 | ui.write(1, "test line 3"); 33 | ui.write(1, "test line 4"); 34 | ui.write(1, "test line 5"); 35 | ui.write(2, "AAAAAAAAA"); 36 | ui.write(2, "BBBBBBBBBBBBBBBBBb"); 37 | ui.write(2, "CCCCC"); 38 | ui.write(2, "DDDDDDDDDDDDD"); 39 | ui.write(2, "EEEEEEEEEEEEEEEEEEEEe"); 40 | ui.write(2, "FFFFFFFFF"); 41 | ui.update(); 42 | }); 43 | 44 | task::block_on(async move { 45 | let mut stdin = std::io::stdin().into_raw_mode().unwrap(); 46 | let mut buf = vec![0]; 47 | let mut exit = false; 48 | 49 | while !exit { 50 | let mut ui = ui.lock().await; 51 | // Read the input from stdin. 52 | stdin.read_exact(&mut buf).unwrap(); 53 | // Collect input. 54 | let lines = { 55 | ui.input.putc(buf[0]); 56 | ui.update(); 57 | let mut lines = vec![]; 58 | while let Some(event) = ui.input.next_event() { 59 | if let InputEvent::Line(line) = event { 60 | lines.push(line); 61 | } 62 | } 63 | lines 64 | }; 65 | // Parse the input. 66 | for line in lines { 67 | let parts = line.split_whitespace().collect::>(); 68 | match parts.get(0) { 69 | // Switch window based on input. 70 | Some(&"/win") | Some(&"/w") => { 71 | let i: usize = parts.get(1).unwrap().parse().unwrap(); 72 | ui.set_active_index(i); 73 | } 74 | // Quit the application. 75 | Some(&"/quit") | Some(&"/exit") => { 76 | exit = true; 77 | break; 78 | } 79 | _ => {} 80 | } 81 | } 82 | 83 | ui.update(); 84 | } 85 | 86 | let mut ui = ui.lock().await; 87 | ui.finish(); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cabin 2 | 3 | A rustic Cabal client. 4 | 5 | Cabin is a command-line Cabal client written in Rust. It uses the [cable.rs](https://github.com/cabal-club/cable.rs) implementation of the new [Cable protocol](https://github.com/cabal-club/cable). 6 | 7 | **Status**: alpha (under active construction; expect changes). 8 | 9 | ## Usage 10 | 11 | Begin by cloning, building and running `cabin`: 12 | 13 | ``` 14 | git clone git@github.com:cabal-club/cabin.git 15 | cd cabin 16 | cargo build --release 17 | ./target/release/cabin 18 | ``` 19 | 20 | ### Add a Cabal 21 | 22 | Once `cabin` has launched, a cabal must be added from the `!status` window. For example: 23 | 24 | `/cabal add 1115a517c5922baa9594f5555c16e091ce4251579818fb4c4f301804c847f222` 25 | 26 | Having at least one active cabal is a prerequisite for many other behaviours and actions of `cabin`. Multiple cabals are supported for each instance of `cabin`. 27 | 28 | ### Listen for TCP Connections 29 | 30 | `cabin` uses TCP to make connections with peers. Start a TCP listener by providing a port and, optionally, an IP or hostname (a default IP of `0.0.0.0` is used when one is not explicitly provided): 31 | 32 | `/listen 8007` 33 | 34 | ### Connect to a Peer Over TCP 35 | 36 | Once you know the IP / hostname and port of a listening `cabin` instance, a connection can be attempted as follows: 37 | 38 | `/connect 25.1.204.77:8007` 39 | 40 | ### Join a Channel 41 | 42 | Channels can be joined using the `/join` / `/j` commands: 43 | 44 | `/join myco` 45 | 46 | ## Help 47 | 48 | From the `!status` window, type `/help` and press `` to display the help menu. If the active window is a channel and you wish to return to the `!status` window, type `/win 0`. 49 | 50 | ``` 51 | [17:58] -status- /help 52 | [17:58] -status- /cabal add ADDR 53 | [17:58] -status- add a cabal 54 | [17:58] -status- /cabal set ADDR 55 | [17:58] -status- set the active cabal 56 | [17:58] -status- /cabal list 57 | [17:58] -status- list all known cabals 58 | [17:58] -status- /channels 59 | [17:58] -status- list all known channels 60 | [17:58] -status- /connections 61 | [17:58] -status- list all known network connections 62 | [17:58] -status- /connect HOST:PORT 63 | [17:58] -status- connect to a peer over tcp 64 | [17:58] -status- /delete nick 65 | [17:58] -status- delete the most recent nick 66 | [17:58] -status- /join CHANNEL 67 | [17:58] -status- join a channel (shorthand: /j CHANNEL) 68 | [17:58] -status- /listen PORT 69 | [17:58] -status- listen for incoming tcp connections on 0.0.0.0 70 | [17:58] -status- /listen HOST:PORT 71 | [17:58] -status- listen for incoming tcp connections 72 | [17:58] -status- /members CHANNEL 73 | [17:58] -status- list all known members of the channel 74 | [17:58] -status- /topic 75 | [17:58] -status- list the topic of the active channel 76 | [17:58] -status- /topic TOPIC 77 | [17:58] -status- set the topic of the active channel 78 | [17:58] -status- /whoami 79 | [17:58] -status- list the local public key as a hex string 80 | [17:58] -status- /win INDEX 81 | [17:58] -status- change the active window (shorthand: /w INDEX) 82 | [17:58] -status- /exit 83 | [17:58] -status- exit the cabal process 84 | [17:58] -status- /quit 85 | [17:58] -status- exit the cabal process (shorthand: /q) 86 | ``` 87 | 88 | ## Logging 89 | 90 | Logging of various levels can be enabled by specifying the `RUST_LOG` environment variable: 91 | 92 | `RUST_LOG=debug ./target/release/cabin` 93 | 94 | It can be helpful to redirect the `stderr` of the process to a file when debugging: 95 | 96 | `RUST_LOG=debug ./target/release/cabin 2> output` 97 | 98 | Or to a terminal: 99 | 100 | First run the command `tty` in an empty terminal. It should return a file path. For example: 101 | 102 | ``` 103 | tty 104 | /dev/pts/2 105 | ``` 106 | 107 | The path can then be used to direct `stderr` to the empty terminal: 108 | 109 | `RUST_LOG=debug ./target/release/cabin 2> /dev/pts/2` 110 | 111 | ## Developer / Contributor Guide 112 | 113 | Wherever possible, idiomatic Rust conventions have been followed regarding code formatting and style. Doc and code comments can be found throughout the codebase and will guide you in any contribution efforts. In addition, there are examples and tests to read and learn from. With all that being said, there is still much room for improvement and contributions are welcome. 114 | 115 | Before beginning work on any contributions, please open an issue introducing what you wish to work on. A project maintainer will respond and help to ensure that the intended contribution is a good fit for the project and that you are supported in your efforts. The issue can later be referenced in any subsequent pull-requests. 116 | 117 | When it comes to code styling, it's recommended to refer to the codebase and follow the established stylistic conventions. This is not a hard requirement but a consistent codebase helps to facilitate clarity and ease of understanding. When in doubt, open an issue to ask for guidance. 118 | 119 | There are many code comments throughout the codebase labelled with `TODO`; these may provide some inspiration for initial contributions. 120 | 121 | ## Credits 122 | 123 | Banner artwork is derived from ASCII artwork by Furtado H. ([source](https://ascii.co.uk/art/cabin)). 124 | 125 | ## Contact 126 | 127 | glyph (glyph@mycelial.technology). 128 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeSet, io::Write}; 2 | 3 | use async_std::sync::{Arc, Mutex}; 4 | use cable::{Channel, Nickname, Text, Timestamp, Topic}; 5 | use owo_colors::OwoColorize; 6 | use signal_hook::{ 7 | consts::SIGWINCH, 8 | iterator::{exfiltrator::WithOrigin, SignalsInfo}, 9 | }; 10 | 11 | use crate::{hex, input::Input, time, utils}; 12 | 13 | pub type Addr = Vec; 14 | pub type PublicKey = [u8; 32]; 15 | pub type TermSize = (u32, u32); 16 | 17 | /// A `BTreeSet` representing the data for each line posted to the UI. 18 | /// Includes a line index, timestamp, optional public key, optional nickname 19 | /// and text. 20 | type LinesSet = BTreeSet<(u64, Timestamp, Option, Option, Text)>; 21 | 22 | /// Determine the dimensions of the terminal. 23 | pub fn get_term_size() -> TermSize { 24 | term_size::dimensions() 25 | .map(|(w, h)| (w as u32, h as u32)) 26 | .unwrap() 27 | } 28 | 29 | /// Resize the user interface to match the dimensions of the terminal. 30 | pub async fn resizer(ui: Arc>) { 31 | let mut signals = SignalsInfo::::new(&vec![SIGWINCH]).unwrap(); 32 | for info in &mut signals { 33 | if info.signal == SIGWINCH { 34 | ui.lock().await.resize(get_term_size()) 35 | } 36 | } 37 | } 38 | 39 | /// A single user-interface window. 40 | pub struct Window { 41 | /// The hex address of a cabal. 42 | pub address: Addr, 43 | /// The channel whose contents the window is displaying. 44 | pub channel: Channel, 45 | /// The channel topic. 46 | pub topic: Topic, 47 | /// The age of the most recent post(s) to be displayed. 48 | pub time_end: u64, 49 | /// The total number of posts which may be displayed. 50 | pub limit: usize, 51 | /// The lines of the window (index, timestamp, author, nickname, text). 52 | pub lines: LinesSet, 53 | /// A line index counter to facilitate line insertions. 54 | line_index: u64, 55 | } 56 | 57 | impl Window { 58 | /// Create a new window with the given address and channel. 59 | pub fn new(address: Addr, channel: Channel) -> Self { 60 | Self { 61 | address, 62 | channel, 63 | topic: String::new(), 64 | time_end: 0, 65 | limit: 50, 66 | lines: BTreeSet::default(), 67 | line_index: 0, 68 | } 69 | } 70 | 71 | /// Write the message to the window. 72 | pub fn write(&mut self, msg: &str) { 73 | self.insert(time::now().unwrap(), None, None, msg); 74 | } 75 | 76 | /// Insert a new line into the window using the given message timestamp, 77 | /// name and text. 78 | /// 79 | /// The name will be the public key of the post author if a name-defining 80 | /// `post/info` is not available. 81 | pub fn insert( 82 | &mut self, 83 | timestamp: Timestamp, 84 | author: Option, 85 | nick: Option, 86 | text: &str, 87 | ) { 88 | let index = self.line_index; 89 | self.line_index += 1; 90 | self.lines 91 | .insert((index, timestamp, author, nick, text.to_string())); 92 | } 93 | 94 | pub fn update_topic(&mut self, topic: String) { 95 | self.topic = topic; 96 | } 97 | } 98 | 99 | pub struct Ui { 100 | pub active_window: usize, 101 | pub active_address: Option, 102 | pub windows: Vec, 103 | pub diff: ansi_diff::Diff, 104 | pub size: TermSize, 105 | pub input: Input, 106 | pub stdout: std::io::Stdout, 107 | tick: u64, 108 | } 109 | 110 | impl Ui { 111 | pub fn new(size: TermSize) -> Self { 112 | let windows = vec![Window::new(vec![], "!status".to_string())]; 113 | 114 | Self { 115 | diff: ansi_diff::Diff::new(size), 116 | size, 117 | active_window: 0, 118 | active_address: None, 119 | windows, 120 | input: Input::default(), 121 | stdout: std::io::stdout(), 122 | tick: 0, 123 | } 124 | } 125 | 126 | pub fn resize(&mut self, size: TermSize) { 127 | self.diff.resize(size); 128 | } 129 | 130 | pub fn get_size(&self) -> TermSize { 131 | self.size 132 | } 133 | 134 | pub fn write_status(&mut self, msg: &str) { 135 | self.windows.get_mut(0).unwrap().write(msg); 136 | } 137 | 138 | pub fn write(&mut self, index: usize, msg: &str) { 139 | self.windows.get_mut(index).unwrap().write(msg); 140 | } 141 | 142 | pub fn get_active_window(&mut self) -> &mut Window { 143 | self.windows.get_mut(self.active_window).unwrap() 144 | } 145 | 146 | pub fn get_active_index(&self) -> usize { 147 | self.active_window 148 | } 149 | 150 | pub fn set_active_index(&mut self, index: usize) { 151 | self.active_window = index.min(self.windows.len().max(1) - 1); 152 | } 153 | 154 | pub fn get_active_address(&self) -> Option<&Addr> { 155 | self.active_address.as_ref() 156 | } 157 | 158 | pub fn set_active_address(&mut self, addr: &Addr) { 159 | self.active_address = Some(addr.clone()); 160 | } 161 | 162 | pub fn add_window(&mut self, address: Addr, channel: Channel) -> usize { 163 | self.windows.push(Window::new(address, channel)); 164 | self.windows.len() - 1 165 | } 166 | 167 | pub fn get_window<'a>( 168 | &'a mut self, 169 | address: &Addr, 170 | channel: &Channel, 171 | ) -> Option<&'a mut Window> { 172 | self.windows 173 | .iter_mut() 174 | .find(|w| &w.address == address && &w.channel == channel) 175 | } 176 | 177 | pub fn get_window_index(&self, address: &Addr, channel: &Channel) -> Option { 178 | self.windows 179 | .iter() 180 | .position(|w| &w.address == address && &w.channel == channel) 181 | } 182 | 183 | pub fn move_window(&mut self, src: usize, dst: usize) { 184 | let w = self.windows.remove(src); 185 | self.windows.insert(dst, w); 186 | } 187 | 188 | pub fn remove_window(&mut self, index: usize) { 189 | self.windows.remove(index); 190 | if index < self.active_window { 191 | self.active_window = self.active_window.min(1) - 1; 192 | } 193 | } 194 | 195 | pub fn update(&mut self) { 196 | // Get the active window. 197 | // TODO: Handle the error case properly. 198 | let window = self.windows.get(self.active_window).unwrap(); 199 | 200 | let mut lines = window 201 | .lines 202 | .iter() 203 | .map(|(_index, timestamp, author, nickname, line)| { 204 | if let Some(public_key) = author { 205 | let colour = utils::public_key_to_colour(public_key); 206 | 207 | // Display the nickname of the post author if one is known. 208 | if let Some(name) = nickname { 209 | format!( 210 | "[{}] <{}> {}", 211 | time::format(*timestamp), 212 | name.color(colour), 213 | line 214 | ) 215 | } else { 216 | // Fallback to displaying the abbreviated public key of 217 | // the author if no nickname is known. 218 | let abbreviated_public_key = hex::to(&public_key[..4]); 219 | format!( 220 | "[{}] <{}> {}", 221 | time::format(*timestamp), 222 | abbreviated_public_key.color(colour), 223 | line 224 | ) 225 | } 226 | } else { 227 | format!( 228 | "[{}] {} {}", 229 | time::format(*timestamp), 230 | "-status-".bright_green(), 231 | line 232 | ) 233 | } 234 | }) 235 | .collect::>(); 236 | 237 | for _ in lines.len()..(self.size.1 as usize) - 2 { 238 | lines.push(String::default()); 239 | } 240 | 241 | let input = { 242 | let c = self.input.cursor.min(self.input.value.len()); 243 | let n = (c + 1).min(self.input.value.len()); 244 | let s = if n > c { &self.input.value[c..n] } else { " " }; 245 | self.input.value[0..c].to_string() + "\x1b[7m" + s + "\x1b[0m" + &self.input.value[n..] 246 | }; 247 | 248 | write!( 249 | self.stdout, 250 | "{}{}", 251 | if self.tick == 0 { "\x1bc\x1b[?25l" } else { "" }, // clear, turn off cursor 252 | self.diff 253 | .update(&format!( 254 | "[{}] {}\n{}\n> {}", 255 | // Display the channel name (!status or other). 256 | if window.channel == "!status" { 257 | format!("{}", window.channel.bright_green()) 258 | } else { 259 | format!("#{}", &window.channel) 260 | }, 261 | // Display the active cabal address. 262 | if window.channel == "!status" && self.active_address.is_some() { 263 | let addr = self.active_address.as_ref().unwrap(); 264 | format!("cabal://{}", hex::to(addr)) 265 | } else if window.channel == "!status" { 266 | "".to_string() 267 | } else { 268 | // Display the channel topic. 269 | window.topic.to_string() 270 | }, 271 | lines.join("\n"), 272 | &input, 273 | )) 274 | .split('\n') 275 | .collect::>() 276 | .join("\r\n"), 277 | ) 278 | .unwrap(); 279 | self.stdout.flush().unwrap(); 280 | self.tick += 1; 281 | } 282 | 283 | pub fn finish(&mut self) { 284 | write!(self.stdout, "\x1bc").unwrap(); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | io::Read, 4 | }; 5 | 6 | use async_std::{ 7 | net, 8 | prelude::*, 9 | sync::{Arc, Mutex}, 10 | task, 11 | }; 12 | use cable::{error::Error, post::PostBody, Channel, ChannelOptions}; 13 | use cable_core::{CableManager, Store}; 14 | use futures::{channel::mpsc, future::AbortHandle, stream::Abortable, SinkExt}; 15 | use log::{debug, error}; 16 | use terminal_keycode::KeyCode; 17 | 18 | use crate::{ 19 | hex, 20 | input::InputEvent, 21 | time, 22 | ui::{Addr, TermSize, Ui}, 23 | }; 24 | 25 | type StorageFn = Box Box>; 26 | 27 | type CloseChannelSender = mpsc::UnboundedSender; 28 | type CloseChannelReceiver = mpsc::UnboundedReceiver; 29 | 30 | /// A TCP connection and associated address (host:post). 31 | #[derive(Debug, Clone, Hash, Eq, PartialEq)] 32 | enum Connection { 33 | Connected(String), 34 | Listening(String), 35 | } 36 | 37 | pub struct App { 38 | abort_handles: Arc>>, 39 | cables: HashMap>, 40 | connections: HashSet, 41 | close_channel_sender: CloseChannelSender, 42 | storage_fn: StorageFn, 43 | pub ui: Arc>, 44 | exit: bool, 45 | } 46 | 47 | impl App 48 | where 49 | S: Store, 50 | { 51 | pub fn new( 52 | size: TermSize, 53 | storage_fn: StorageFn, 54 | close_channel_sender: CloseChannelSender, 55 | ) -> Self { 56 | Self { 57 | abort_handles: Arc::new(Mutex::new(HashMap::new())), 58 | cables: HashMap::new(), 59 | connections: HashSet::new(), 60 | close_channel_sender, 61 | storage_fn, 62 | ui: Arc::new(Mutex::new(Ui::new(size))), 63 | exit: false, 64 | } 65 | } 66 | 67 | /// Listen for "close channel" messages and abort the associated task 68 | /// responsible for updating the UI with posts from the given channel. 69 | /// This prevents double-posting to the UI if a channel is left and then 70 | /// later rejoined. 71 | /// 72 | /// A "close channel" message is sent when the `close_channel()` handler 73 | /// is invoked. 74 | async fn launch_abort_listener(&mut self, mut close_channel_receiver: CloseChannelReceiver) { 75 | let abort_handles = self.abort_handles.clone(); 76 | 77 | task::spawn(async move { 78 | while let Some(close_channel) = close_channel_receiver.next().await { 79 | let abort_handles = abort_handles.lock().await; 80 | if let Some(handle) = abort_handles.get(&close_channel) { 81 | debug!("Aborting post display task for channel {:?}", close_channel); 82 | handle.abort(); 83 | } 84 | } 85 | }); 86 | } 87 | 88 | /// Add the given cabal address (key) to the cable manager. 89 | pub fn add_cable(&mut self, addr: &Addr) { 90 | let s_addr = hex::to(addr); 91 | self.cables.insert( 92 | addr.to_vec(), 93 | CableManager::new(*(self.storage_fn)(&s_addr)), 94 | ); 95 | } 96 | 97 | /// Return the address and manager for the active cable. 98 | pub async fn get_active_cable(&mut self) -> Option<(Addr, CableManager)> { 99 | self.ui 100 | .lock() 101 | .await 102 | .get_active_address() 103 | .and_then(|addr| self.cables.get(addr).map(|c| (addr.clone(), c.clone()))) 104 | } 105 | 106 | /// Set the address (key) of the active cabal. 107 | pub async fn set_active_address(&self, addr: &Addr) { 108 | self.ui.lock().await.set_active_address(addr); 109 | } 110 | 111 | /// Get the address (key) of the active cabal. 112 | pub async fn get_active_address(&self) -> Option { 113 | self.ui.lock().await.get_active_address().cloned() 114 | } 115 | 116 | /// Handle the `/cabal` commands. 117 | /// 118 | /// Adds a new cabal, sets the active cabal or lists all known cabals. 119 | // TODO: Split this into multiple handler, one per subcommand. 120 | async fn cabal_handler(&mut self, args: Vec) { 121 | match (args.get(1).map(|x| x.as_str()), args.get(2)) { 122 | (Some("add"), Some(hex_addr)) => { 123 | if let Some(addr) = hex::from(hex_addr) { 124 | self.add_cable(&addr); 125 | self.write_status(&format!("added cabal: {}", hex_addr)) 126 | .await; 127 | self.set_active_address(&addr).await; 128 | self.write_status(&format!("set active cabal to {}", hex_addr)) 129 | .await; 130 | } else { 131 | self.write_status(&format!("invalid cabal address: {}", hex_addr)) 132 | .await; 133 | } 134 | } 135 | (Some("add"), None) => { 136 | self.write_status("usage: /cabal add ADDR").await; 137 | } 138 | (Some("set"), Some(s_addr)) => { 139 | if let Some(addr) = hex::from(s_addr) { 140 | self.set_active_address(&addr).await; 141 | self.write_status(&format!("set active cabal to {}", s_addr)) 142 | .await; 143 | } else { 144 | self.write_status(&format!("invalid cabal address: {}", s_addr)) 145 | .await; 146 | } 147 | } 148 | (Some("set"), None) => { 149 | self.write_status("usage: /cabal set ADDR").await; 150 | } 151 | (Some("list"), _) => { 152 | for addr in self.cables.keys() { 153 | let is_active = self 154 | .get_active_address() 155 | .await 156 | .map(|x| &x == addr) 157 | .unwrap_or(false); 158 | let star = if is_active { "*" } else { "" }; 159 | self.write_status(&format!("{}{}", hex::to(addr), star)) 160 | .await; 161 | } 162 | if self.cables.is_empty() { 163 | self.write_status("{ no cabals in list }").await; 164 | } 165 | } 166 | _ => {} 167 | } 168 | } 169 | 170 | /// Handle the `/channels` command. 171 | /// 172 | /// Prints a list of known channels for the active cable instance. 173 | async fn channels_handler(&mut self) { 174 | if let Some((_address, cable)) = self.get_active_cable().await { 175 | let mut ui = self.ui.lock().await; 176 | if let Some(channels) = cable.store.get_channels().await { 177 | for channel in channels { 178 | ui.write_status(&format!("- {}", channel)); 179 | } 180 | } else { 181 | ui.write_status("{ no known channels for the active cabal }"); 182 | } 183 | ui.update(); 184 | } else { 185 | let mut ui = self.ui.lock().await; 186 | ui.write_status(&format!( 187 | "{}{}", 188 | "cannot list channels with no active cabal set.", 189 | " add a cabal with \"/cabal add\" first", 190 | )); 191 | ui.update(); 192 | } 193 | } 194 | 195 | /// Handle the `/connect` command. 196 | /// 197 | /// Attempts a TCP connection to the given host:port. 198 | async fn connect_handler(&mut self, args: Vec) { 199 | if self.get_active_address().await.is_none() { 200 | self.write_status(r#"no active cabal to bind this connection. use "/cabal add" first"#) 201 | .await; 202 | } else if let Some(tcp_addr) = args.get(1).cloned() { 203 | // Retrieve the active cable manager. 204 | let (_, cable) = self.get_active_cable().await.unwrap(); 205 | 206 | let ui = self.ui.clone(); 207 | 208 | // Register the connection. 209 | self.connections 210 | .insert(Connection::Connected(tcp_addr.clone())); 211 | 212 | // Attempt a TCP connection to the peer and invoke the 213 | // cable listener. 214 | task::spawn(async move { 215 | let stream = net::TcpStream::connect(tcp_addr.clone()).await?; 216 | 217 | // This block expression is needed to drop the lock and prevent 218 | // blocking of the UI. 219 | { 220 | // Update the UI. 221 | let mut ui = ui.lock().await; 222 | ui.write_status(&format!("connected to {}", tcp_addr)); 223 | ui.update(); 224 | } 225 | 226 | cable.listen(stream).await?; 227 | 228 | // Type inference fails without binding concretely to `Result`. 229 | Result::<(), Error>::Ok(()) 230 | }); 231 | } else { 232 | // Print usage example for the connect command. 233 | let mut ui = self.ui.lock().await; 234 | ui.write_status("usage: /connect HOST:PORT"); 235 | ui.update(); 236 | } 237 | } 238 | 239 | /// Handle the `/connections` command. 240 | /// 241 | /// Prints a list of active TCP connections. 242 | async fn connections_handler(&mut self) { 243 | let mut ui = self.ui.lock().await; 244 | for connection in self.connections.iter() { 245 | ui.write_status(&match connection { 246 | Connection::Connected(addr) => format!("connected to {}", addr), 247 | Connection::Listening(addr) => format!("listening on {}", addr), 248 | }); 249 | } 250 | if self.connections.is_empty() { 251 | ui.write_status("{ no connections in list }"); 252 | } 253 | ui.update(); 254 | } 255 | 256 | /// Handle the `/delete` command. 257 | /// 258 | /// Deletes the most recently set nickname for the local peer. 259 | async fn delete_handler(&mut self, args: Vec) -> Result<(), Error> { 260 | if let Some((_address, mut cable)) = self.get_active_cable().await { 261 | if let Some("nick") = args.get(1).map(|arg| arg.as_str()) { 262 | if let Some((public_key, _private_key)) = cable.store.get_keypair().await { 263 | if let Some((_name, hash)) = 264 | cable.store.get_peer_name_and_hash(&public_key).await 265 | { 266 | cable.post_delete(vec![hash]).await?; 267 | let mut ui = self.ui.lock().await; 268 | ui.write_status("deleted most recent nickname"); 269 | ui.update(); 270 | } else { 271 | let mut ui = self.ui.lock().await; 272 | ui.write_status("no nickname found for the local peer"); 273 | ui.update(); 274 | } 275 | } 276 | } else { 277 | self.write_status("usage: /delete nick").await; 278 | } 279 | } else { 280 | let mut ui = self.ui.lock().await; 281 | ui.write_status(&format!( 282 | "{}{}", 283 | "cannot delete nickname with no active cabal set.", 284 | " add a cabal with \"/cabal add\" first", 285 | )); 286 | ui.update(); 287 | } 288 | Ok(()) 289 | } 290 | 291 | /// Handle the `/help` command. 292 | /// 293 | /// Prints a description and usage example for all commands. 294 | async fn help_handler(&mut self) { 295 | let mut ui = self.ui.lock().await; 296 | ui.write_status("/cabal add ADDR"); 297 | ui.write_status(" add a cabal"); 298 | ui.write_status("/cabal set ADDR"); 299 | ui.write_status(" set the active cabal"); 300 | ui.write_status("/cabal list"); 301 | ui.write_status(" list all known cabals"); 302 | ui.write_status("/channels"); 303 | ui.write_status(" list all known channels"); 304 | ui.write_status("/connections"); 305 | ui.write_status(" list all known network connections"); 306 | ui.write_status("/connect HOST:PORT"); 307 | ui.write_status(" connect to a peer over tcp"); 308 | ui.write_status("/delete nick"); 309 | ui.write_status(" delete the most recent nick"); 310 | ui.write_status("/join CHANNEL"); 311 | ui.write_status(" join a channel (shorthand: /j CHANNEL)"); 312 | ui.write_status("/listen PORT"); 313 | ui.write_status(" listen for incoming tcp connections on 0.0.0.0"); 314 | ui.write_status("/listen HOST:PORT"); 315 | ui.write_status(" listen for incoming tcp connections"); 316 | ui.write_status("/members CHANNEL"); 317 | ui.write_status(" list all known members of the channel"); 318 | ui.write_status("/topic"); 319 | ui.write_status(" list the topic of the active channel"); 320 | ui.write_status("/topic TOPIC"); 321 | ui.write_status(" set the topic of the active channel"); 322 | ui.write_status("/whoami"); 323 | ui.write_status(" list the local public key as a hex string"); 324 | ui.write_status("/win INDEX"); 325 | ui.write_status(" change the active window (shorthand: /w INDEX)"); 326 | ui.write_status("/exit"); 327 | ui.write_status(" exit the cabal process"); 328 | ui.write_status("/quit"); 329 | ui.write_status(" exit the cabal process (shorthand: /q)"); 330 | ui.update(); 331 | } 332 | 333 | /// Handle the `/join` and `/j` commands. 334 | /// 335 | /// Sets the active window of the UI, publishes a `post/join` if the local 336 | /// peer is not already a channel member, creates a channel time range 337 | /// request and updates the UI with stored and received posts. 338 | async fn join_handler(&mut self, args: Vec) -> Result<(), Error> { 339 | if let Some((address, mut cable)) = self.get_active_cable().await { 340 | if let Some(channel) = args.get(1) { 341 | // Check if the local peer is already a member of this channel. 342 | // If not, publish a `post/join` post. 343 | if let Some((public_key, _private_key)) = cable.store.get_keypair().await { 344 | if !cable.store.is_channel_member(channel, &public_key).await { 345 | // TODO: Match on validation error and display to user. 346 | cable.post_join(channel).await?; 347 | } 348 | } 349 | 350 | let mut ui = self.ui.lock().await; 351 | let channel_window_index = ui.get_window_index(&address, channel); 352 | 353 | // Define the window index. 354 | // 355 | // First check if a window has previously been created for the 356 | // given address / channel combination. If so, return the 357 | // index. Otherwise, add a new window and return the index. 358 | let index = channel_window_index 359 | .unwrap_or_else(|| ui.add_window(address.clone(), channel.clone())); 360 | 361 | let ch = channel.clone(); 362 | 363 | ui.set_active_index(index); 364 | ui.update(); 365 | // The UI remains locked if not explicitly dropped here. 366 | drop(ui); 367 | 368 | // Define the channel options. 369 | let opts = ChannelOptions { 370 | channel: ch.clone(), 371 | time_start: time::two_weeks_ago()?, 372 | time_end: 0, 373 | limit: 4096, 374 | }; 375 | 376 | let store = cable.store.clone(); 377 | let ui = self.ui.clone(); 378 | let mut ui = ui.lock().await; 379 | 380 | // Open the channel and update the UI with stored and received 381 | // text posts; only if this action has not been performed 382 | // previously. 383 | // 384 | // The window index is used as a proxy for "channel has been 385 | // initialised". 386 | if channel_window_index.is_none() { 387 | ui.write_status(&format!("joined channel {}", channel)); 388 | ui.update(); 389 | 390 | let mut stored_posts_stream = cable.store.get_posts(&opts).await; 391 | while let Some(post_stream) = stored_posts_stream.next().await { 392 | if let Ok(post) = post_stream { 393 | let timestamp = post.header.timestamp; 394 | let public_key = post.header.public_key; 395 | let nickname = store 396 | .get_peer_name_and_hash(&public_key) 397 | .await 398 | .map(|(nick, _hash)| nick); 399 | 400 | if let PostBody::Text { channel, text } = post.body { 401 | if let Some(window) = ui.get_window(&address, &channel) { 402 | window.insert(timestamp, Some(public_key), nickname, &text); 403 | ui.update(); 404 | } 405 | } else if let PostBody::Topic { channel, topic } = post.body { 406 | if let Some(window) = ui.get_window(&address, &channel) { 407 | window.update_topic(topic); 408 | ui.update(); 409 | } 410 | } 411 | } 412 | } 413 | drop(stored_posts_stream); 414 | 415 | // Create an abort handle and add it to the local map. 416 | // 417 | // This allows the `display_posts` task to be aborted 418 | // when the channel is left, thereby preventing double 419 | // posting to the UI if the channel is later rejoined. 420 | let (abort_handle, abort_registration) = AbortHandle::new_pair(); 421 | self.abort_handles 422 | .lock() 423 | .await 424 | .insert(channel.to_owned(), abort_handle); 425 | 426 | let store = cable.store.clone(); 427 | 428 | let ui = self.ui.clone(); 429 | let display_posts = async move { 430 | let mut stream = cable 431 | .open_channel(&opts) 432 | .await 433 | // TODO: Can we handle this unwrap another way? 434 | .unwrap(); 435 | 436 | while let Some(post_stream) = stream.next().await { 437 | if let Ok(post) = post_stream { 438 | let timestamp = post.header.timestamp; 439 | let public_key = post.header.public_key; 440 | let nickname = store 441 | .get_peer_name_and_hash(&public_key) 442 | .await 443 | .map(|(nick, _hash)| nick); 444 | 445 | if let PostBody::Text { channel, text } = post.body { 446 | let mut ui = ui.lock().await; 447 | if let Some(window) = ui.get_window(&address, &channel) { 448 | window.insert(timestamp, Some(public_key), nickname, &text); 449 | ui.update(); 450 | } 451 | } else if let PostBody::Topic { channel, topic } = post.body { 452 | let mut ui = ui.lock().await; 453 | if let Some(window) = ui.get_window(&address, &channel) { 454 | window.update_topic(topic); 455 | ui.update(); 456 | } 457 | } 458 | } 459 | } 460 | }; 461 | 462 | task::spawn(Abortable::new(display_posts, abort_registration)); 463 | } 464 | } else { 465 | let mut ui = self.ui.lock().await; 466 | ui.write_status("usage: /join CHANNEL"); 467 | ui.update(); 468 | } 469 | } else { 470 | let mut ui = self.ui.lock().await; 471 | ui.write_status(&format!( 472 | "{}{}", 473 | "cannot join channel with no active cabal set.", 474 | " add a cabal with \"/cabal add\" first", 475 | )); 476 | ui.update(); 477 | } 478 | 479 | Ok(()) 480 | } 481 | 482 | /// Handle the `/leave` command. 483 | /// 484 | /// Cancels any active outbound channel time range requests for the 485 | /// given channel and publishes a `post/leave`. 486 | async fn leave_handler(&mut self, args: Vec) -> Result<(), Error> { 487 | if let Some((address, mut cable)) = self.get_active_cable().await { 488 | if let Some(channel) = args.get(1) { 489 | if let Some(channels) = cable.store.get_channels().await { 490 | // Avoid closing and leaving a channel that isn't known to the 491 | // local peer. 492 | if channels.contains(channel) { 493 | // Cancel any active outbound channel time range requests 494 | // for this channel. 495 | cable.close_channel(channel).await?; 496 | 497 | // Check if the local peer is a member of this channel. 498 | // If so, publish a `post/leave` post. 499 | if let Some((public_key, _private_key)) = cable.store.get_keypair().await { 500 | if cable.store.is_channel_member(channel, &public_key).await { 501 | // TODO: Match on validation error and display to user. 502 | cable.post_leave(channel).await?; 503 | } 504 | } 505 | 506 | self.close_channel_sender.send(channel.to_owned()).await?; 507 | 508 | let mut ui = self.ui.lock().await; 509 | // Remove the window associated with the given channel. 510 | if let Some(index) = ui.get_window_index(&address, channel) { 511 | ui.remove_window(index) 512 | } 513 | // Return to the home / status window. 514 | ui.set_active_index(0); 515 | ui.write_status(&format!("left channel {}", channel)); 516 | ui.update(); 517 | } 518 | } else { 519 | let mut ui = self.ui.lock().await; 520 | ui.write_status(&format!( 521 | "not currently a member of channel {}; no action taken", 522 | channel 523 | )); 524 | ui.update(); 525 | } 526 | } else { 527 | let mut ui = self.ui.lock().await; 528 | ui.write_status("usage: /leave CHANNEL"); 529 | ui.update(); 530 | } 531 | } else { 532 | let mut ui = self.ui.lock().await; 533 | ui.write_status(&format!( 534 | "{}{}", 535 | "cannot leave channel with no active cabal set.", 536 | " add a cabal with \"/cabal add\" first", 537 | )); 538 | ui.update(); 539 | } 540 | 541 | Ok(()) 542 | } 543 | 544 | /// Handle the `/listen` command. 545 | /// 546 | /// Deploys a TCP server on the given host:port, listens for incoming 547 | /// connections and passes any resulting streams to the cable manager. 548 | async fn listen_handler(&mut self, args: Vec) { 549 | // Retrieve the active cable address (aka. key). 550 | if self.get_active_address().await.is_none() { 551 | self.write_status(r#"no active cabal to bind this connection. use "/cabal add" first"#) 552 | .await; 553 | } else if let Some(mut tcp_addr) = args.get(1).cloned() { 554 | // Format the TCP address if a host was not supplied. 555 | if !tcp_addr.contains(':') { 556 | tcp_addr = format!("0.0.0.0:{}", tcp_addr); 557 | } 558 | 559 | // Retrieve the active cable manager. 560 | let (_, cable) = self.get_active_cable().await.unwrap(); 561 | 562 | // Register the listener. 563 | self.connections 564 | .insert(Connection::Listening(tcp_addr.clone())); 565 | 566 | let ui = self.ui.clone(); 567 | 568 | task::spawn(async move { 569 | let listener = net::TcpListener::bind(tcp_addr.clone()).await.unwrap(); 570 | 571 | // Update the UI. 572 | let mut ui = ui.lock().await; 573 | ui.write_status(&format!("listening on {}", tcp_addr)); 574 | ui.update(); 575 | drop(ui); 576 | 577 | debug!("Listening for incoming TCP connections..."); 578 | 579 | // Listen for incoming TCP connections and spawn a 580 | // cable listener for each stream. 581 | let mut incoming = listener.incoming(); 582 | while let Some(stream) = incoming.next().await { 583 | debug!("Received an incoming TCP connection"); 584 | if let Ok(stream) = stream { 585 | let cable = cable.clone(); 586 | task::spawn(async move { 587 | if let Err(err) = cable.listen(stream).await { 588 | error!("Cable stream listener error: {}", err); 589 | } 590 | }); 591 | } 592 | } 593 | }); 594 | } else { 595 | // Print usage example for the listen command. 596 | let mut ui = self.ui.lock().await; 597 | ui.write_status("usage: /listen (ADDR:)PORT"); 598 | ui.update(); 599 | } 600 | } 601 | 602 | /// Handle the `/members` command. 603 | /// 604 | /// Prints a list of known members of a channel. If this handler is invoked 605 | /// from an active channel window, the members of that channel will be 606 | /// printed. Otherwise, the handler can be invoked with a specific channel 607 | /// name as an argument; this is useful for printing channel members when 608 | /// the status window is active. 609 | async fn members_handler(&mut self, args: Vec) { 610 | if let Some((_address, cable)) = self.get_active_cable().await { 611 | if let Some(channel) = args.get(1) { 612 | let mut ui = self.ui.lock().await; 613 | 614 | if let Some(members) = cable.store.get_channel_members(channel).await { 615 | for member in members { 616 | // Retrieve and print the nick for each member's 617 | // public key. 618 | if let Some((name, _hash)) = 619 | cable.store.get_peer_name_and_hash(&member).await 620 | { 621 | ui.write_status(&format!(" {}", name)); 622 | } else { 623 | // Fall back to the public key (formatted as a 624 | // hex string) if no nick is known. 625 | ui.write_status(&format!(" {}", hex::to(&member))); 626 | } 627 | } 628 | } else { 629 | ui.write_status( 630 | "{ no known channel members for the active cabal and channel }", 631 | ); 632 | } 633 | ui.update(); 634 | } else { 635 | // No args were passed to the `/members` handler. Attempt to 636 | // determine the channel for the active window and print the 637 | // members. 638 | let mut ui = self.ui.lock().await; 639 | let index = ui.get_active_index(); 640 | // Don't attempt to retrieve and print channel members if the 641 | // status window is active. 642 | if index != 0 { 643 | let window = ui.get_active_window(); 644 | if let Some(members) = cable.store.get_channel_members(&window.channel).await { 645 | for member in members { 646 | // Retrieve and print the nick for each member's 647 | // public key. 648 | if let Some((name, _hash)) = 649 | cable.store.get_peer_name_and_hash(&member).await 650 | { 651 | ui.write_status(&format!(" {}", name)); 652 | } else { 653 | // Fall back to the public key (formatted as a 654 | // hex string) if no nick is known. 655 | ui.write_status(&format!(" {}", hex::to(&member))); 656 | } 657 | } 658 | } else { 659 | ui.write_status( 660 | "{ no known channel members for the active cabal and channel }", 661 | ); 662 | } 663 | ui.update(); 664 | } 665 | }; 666 | } else { 667 | let mut ui = self.ui.lock().await; 668 | ui.write_status(&format!( 669 | "{}{}", 670 | "cannot list channel members with no active cabal set.", 671 | " add a cabal with \"/cabal add\" first", 672 | )); 673 | ui.update(); 674 | } 675 | } 676 | 677 | /// Handle the `/nick` command. 678 | /// 679 | /// Set the nickname for the local peer. 680 | async fn nick_handler(&mut self, args: Vec) -> Result<(), Error> { 681 | if let Some((_address, mut cable)) = self.get_active_cable().await { 682 | if let Some(nick) = args.get(1) { 683 | let mut ui = self.ui.lock().await; 684 | let _hash = cable.post_info_name(nick).await?; 685 | ui.write_status(&format!("nickname set to {:?}", nick)); 686 | ui.update(); 687 | } else { 688 | let mut ui = self.ui.lock().await; 689 | ui.write_status("usage: /nick NAME"); 690 | ui.update(); 691 | } 692 | } else { 693 | let mut ui = self.ui.lock().await; 694 | ui.write_status(&format!( 695 | "{}{}", 696 | "cannot assign nickname with no active cabal set.", 697 | " add a cabal with \"/cabal add\" first", 698 | )); 699 | ui.update(); 700 | } 701 | 702 | Ok(()) 703 | } 704 | 705 | /// Handle the `/topic` command. 706 | /// 707 | /// Sets the topic of the active channel. 708 | async fn topic_handler(&mut self, args: Vec) -> Result<(), Error> { 709 | if let Some((_address, mut cable)) = self.get_active_cable().await { 710 | if args.get(1).is_some() { 711 | // Get all arguments that come after the `/topic` argument. 712 | let topic: String = args[1..].join(" "); 713 | let mut ui = self.ui.lock().await; 714 | let active_channel = ui.get_active_window().channel.to_owned(); 715 | if active_channel != "!status" { 716 | cable.post_topic(&active_channel, &topic).await?; 717 | ui.write_status(&format!( 718 | "topic set to {:?} for channel {:?}", 719 | topic, active_channel 720 | )); 721 | ui.update(); 722 | } else { 723 | ui.write_status("topic cannot be set for !status window"); 724 | ui.update(); 725 | } 726 | } else { 727 | let mut ui = self.ui.lock().await; 728 | ui.write_status("usage: /topic TOPIC"); 729 | ui.update(); 730 | } 731 | } 732 | 733 | Ok(()) 734 | } 735 | 736 | /// Handle the `/whoami` command. 737 | /// 738 | /// Prints the hex-encoded public key of the local peer. 739 | async fn whoami_handler(&mut self) { 740 | if let Some((_address, cable)) = self.get_active_cable().await { 741 | if let Some((public_key, _private_key)) = cable.store.get_keypair().await { 742 | let mut ui = self.ui.lock().await; 743 | ui.write_status(&format!(" {}", hex::to(&public_key))); 744 | ui.update(); 745 | } 746 | } else { 747 | let mut ui = self.ui.lock().await; 748 | ui.write_status(&format!( 749 | "{}{}", 750 | "cannot list the local public key with no active cabal set.", 751 | " add a cabal with \"/cabal add\" first", 752 | )); 753 | ui.update(); 754 | } 755 | } 756 | 757 | /// Handle the `/win` and `/w` commands. 758 | /// 759 | /// Sets the active window of the UI. 760 | async fn win_handler(&mut self, args: Vec) { 761 | let mut ui = self.ui.lock().await; 762 | if let Some(index) = args.get(1) { 763 | if let Ok(i) = index.parse() { 764 | ui.set_active_index(i); 765 | ui.update(); 766 | } else { 767 | ui.write_status("window index must be a number"); 768 | ui.update(); 769 | } 770 | } else { 771 | ui.write_status("usage: /win INDEX"); 772 | ui.update(); 773 | } 774 | } 775 | 776 | /// Parse UI input and invoke the appropriate handler. 777 | pub async fn handle(&mut self, line: &str) -> Result<(), Error> { 778 | let args = line 779 | .split_whitespace() 780 | .map(|s| s.to_string()) 781 | .collect::>(); 782 | if args.is_empty() { 783 | return Ok(()); 784 | } 785 | 786 | match args.get(0).unwrap().as_str() { 787 | "/cabal" => { 788 | self.write_status(line).await; 789 | self.cabal_handler(args).await; 790 | } 791 | "/channels" => { 792 | self.write_status(line).await; 793 | self.channels_handler().await; 794 | } 795 | "/connect" => { 796 | self.write_status(line).await; 797 | self.connect_handler(args).await; 798 | } 799 | "/connections" => { 800 | self.write_status(line).await; 801 | self.connections_handler().await; 802 | } 803 | "/delete" => { 804 | self.write_status(line).await; 805 | self.delete_handler(args).await?; 806 | } 807 | "/help" => { 808 | self.write_status(line).await; 809 | self.help_handler().await; 810 | } 811 | "/join" | "/j" => { 812 | self.join_handler(args).await?; 813 | } 814 | "/leave" => { 815 | self.leave_handler(args).await?; 816 | } 817 | "/listen" => { 818 | self.write_status(line).await; 819 | self.listen_handler(args).await; 820 | } 821 | "/members" => { 822 | self.write_status(line).await; 823 | self.members_handler(args).await; 824 | } 825 | "/nick" => { 826 | self.write_status(line).await; 827 | self.nick_handler(args).await?; 828 | } 829 | "/topic" => { 830 | self.write_status(line).await; 831 | self.topic_handler(args).await?; 832 | } 833 | "/quit" | "/exit" | "/q" => { 834 | self.write_status(line).await; 835 | self.exit = true; 836 | } 837 | "/whoami" => { 838 | self.write_status(line).await; 839 | self.whoami_handler().await; 840 | } 841 | "/win" | "/w" => { 842 | self.win_handler(args).await; 843 | } 844 | x => { 845 | if x.starts_with('/') { 846 | self.write_status(line).await; 847 | self.write_status(&format!("no such command: {}", x)).await; 848 | } else { 849 | self.post(&line.trim_end().to_string()).await?; 850 | } 851 | } 852 | } 853 | 854 | Ok(()) 855 | } 856 | 857 | /// Post the given text message to the channel and cabal associated with 858 | /// the active UI window. 859 | pub async fn post(&mut self, msg: &String) -> Result<(), Error> { 860 | let mut ui = self.ui.lock().await; 861 | let w = ui.get_active_window(); 862 | if w.channel == "!status" { 863 | ui.write_status("can't post text in status channel. see /help for command list"); 864 | ui.update(); 865 | } else { 866 | let cable = self.cables.get_mut(&w.address).unwrap(); 867 | // TODO: Match on validation error and display to user. 868 | cable.post_text(&w.channel, msg).await?; 869 | } 870 | Ok(()) 871 | } 872 | 873 | /// Run the application. 874 | /// 875 | /// Handle input and update the UI. 876 | pub async fn run( 877 | &mut self, 878 | mut reader: Box, 879 | close_channel_receiver: CloseChannelReceiver, 880 | ) -> Result<(), Error> { 881 | self.launch_abort_listener(close_channel_receiver).await; 882 | 883 | self.ui.lock().await.update(); 884 | self.write_status_banner().await; 885 | 886 | let mut buf = vec![0]; 887 | while !self.exit { 888 | // Parse input from stdin. 889 | reader.read_exact(&mut buf).unwrap(); 890 | let lines = { 891 | let mut ui = self.ui.lock().await; 892 | ui.input.putc(buf[0]); 893 | ui.update(); 894 | let mut lines = vec![]; 895 | while let Some(event) = ui.input.next_event() { 896 | match event { 897 | // TODO: Handle PageUp and PageDown. 898 | InputEvent::KeyCode(KeyCode::PageUp) => {} 899 | InputEvent::KeyCode(KeyCode::PageDown) => {} 900 | InputEvent::KeyCode(_) => {} 901 | InputEvent::Line(line) => { 902 | lines.push(line); 903 | } 904 | } 905 | } 906 | lines 907 | }; 908 | 909 | // Invoke the handler for each line of input. 910 | for line in lines { 911 | self.handle(&line).await?; 912 | if self.exit { 913 | break; 914 | } 915 | } 916 | } 917 | self.ui.lock().await.finish(); 918 | 919 | Ok(()) 920 | } 921 | 922 | /// Update the UI. 923 | pub async fn update(&self) { 924 | self.ui.lock().await.update(); 925 | } 926 | 927 | /// Write the given message to the UI. 928 | pub async fn write_status(&self, msg: &str) { 929 | let mut ui = self.ui.lock().await; 930 | ui.write_status(msg); 931 | ui.update(); 932 | } 933 | 934 | /// Write the welcome banner to the status window. 935 | pub async fn write_status_banner(&mut self) { 936 | // Include the welcome banner at compile time. 937 | let banner = include_str!("../welcome.txt"); 938 | 939 | let mut ui = self.ui.lock().await; 940 | for line in banner.lines() { 941 | ui.write_status(line) 942 | } 943 | ui.update(); 944 | } 945 | } 946 | --------------------------------------------------------------------------------