FULL REWORK AND PORT TO TERMION
This patch is simply meant to mark the beginning of the newest phase for the tui build. I've 100% settled on using termion as the backend and marking what is done so far Renderer side: Termion has a similar issue where moving data happens very quietly so its best if the two (renderer and cache) have the their data to use as they please Cache Side: Basically we own the data we're using because we constantly have to mutate data ourselves Config in the middle: Mutable but only from the rendering side because the cache is completely transient It technically to own its data but it does anyway because the render(backend) likes to consume data like there's no tomorrow
This commit is contained in:
parent
c3d5c75cc0
commit
cb5975f235
786
tui/Cargo.lock
generated
786
tui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,16 +8,18 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
|
||||
clap = "2.33.3"
|
||||
cursive = { version = "0.15", features=["toml"] }
|
||||
# UI Backend
|
||||
tui = "0.14"
|
||||
termion = "1.5"
|
||||
unicode-width = "0.1.5"
|
||||
|
||||
# Better options support
|
||||
clap = "2.33.3"
|
||||
|
||||
# For json
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.114", features = ["derive"] }
|
||||
|
||||
reqwest = "0.11.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
|
||||
[patch.crates-io.enumset_derive]
|
||||
git = "https://github.com/ocboogie/enumset"
|
||||
branch = "span-fix"
|
||||
# Networking related dependancies
|
||||
tokio = { version = "1", features = ["rt", "net", "macros", "rt-multi-thread"] }
|
||||
reqwest = { version = "0.11.2", features = [ "json", "blocking"] }
|
||||
|
32
tui/src/api_types.rs
Normal file
32
tui/src/api_types.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[allow(dead_code)]
|
||||
const VOICE_CHANNEL: i32 = 1;
|
||||
#[allow(dead_code)]
|
||||
const TEXT_CHANNEL: i32 = 2;
|
||||
|
||||
// Network Types
|
||||
#[allow(dead_code)]
|
||||
pub struct Message {
|
||||
pub id: u64,
|
||||
pub time: i64,
|
||||
pub content: String,
|
||||
pub content_type: String,
|
||||
pub channel_id: u64,
|
||||
pub userid: u64,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Channel {
|
||||
pub name: String,
|
||||
pub id: u64,
|
||||
pub type_: i32,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Jwt {
|
||||
pub jwt: String
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// Deals with logging in and requesting new jwt's when required
|
||||
|
||||
|
||||
async fn login() {}
|
||||
|
||||
async fn refresh_jwt() {}
|
58
tui/src/cache.rs
Normal file
58
tui/src/cache.rs
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* welcum to the cache zone
|
||||
* Notes about data model here
|
||||
* Basically none of the ever gets written to disk so its mutability is
|
||||
*
|
||||
* Memory Model things
|
||||
* The cache should always own its own data and nothing else
|
||||
* On calls where the cache system needs to take data it should actually
|
||||
* make its own copy to not disturb the render side of things as it too requires
|
||||
* ownership of its own data. For this reason all parts of this are basically
|
||||
* going to be really selfish about its own data
|
||||
*
|
||||
*/
|
||||
|
||||
use crate::config::{ ServerMeta, UserConfig };
|
||||
use crate::api_types::{Channel, Message};
|
||||
use crate::command::Command;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct ChannelCache {
|
||||
meta: Channel,
|
||||
messages: Vec<Message>
|
||||
}
|
||||
|
||||
struct ServerCache {
|
||||
meta: ServerMeta,
|
||||
user: UserConfig,
|
||||
channels: Vec<ChannelCache>
|
||||
}
|
||||
|
||||
pub struct Cache {
|
||||
// Hostname -> Cache
|
||||
servers: HashMap<String, ServerCache>,
|
||||
active_server: Option<String>
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Cache {
|
||||
Cache {
|
||||
servers: HashMap::new(),
|
||||
active_server: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub async fn switch_channel(&mut self, id: u64) -> Command {
|
||||
todo!()
|
||||
}
|
||||
pub async fn switch_server(&mut self, host: &str) -> Command {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn send_message(&mut self, id: &str) -> Command {
|
||||
todo!()
|
||||
}
|
||||
}
|
94
tui/src/command.rs
Normal file
94
tui/src/command.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use tui::text::{Span, Spans};
|
||||
use tui::style::{Style, Modifier};
|
||||
|
||||
pub enum Command {
|
||||
Help,
|
||||
// Picking based on id
|
||||
Channel(u64),
|
||||
// Choose server based on hostname
|
||||
Server(String),
|
||||
// Send regular message
|
||||
Message(String),
|
||||
// Command that failed with some message
|
||||
Failure(&'static str),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
// Pulls out channel id from a line
|
||||
// Examples: /chan 123
|
||||
// /channel 789
|
||||
// /channelswag 1
|
||||
fn parse_chan_id(s: &str) -> Option<u64> {
|
||||
let parts: Vec<&str> = s.split(" ").collect();
|
||||
return if parts.len() < 2 {
|
||||
None
|
||||
} else {
|
||||
let id_s = parts.get(1).unwrap();
|
||||
match id_s.parse::<u64>() {
|
||||
Ok(id) => Some(id),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hostname(s: &str) -> Option<String> {
|
||||
let parts: Vec<&str> = s.split(" ").collect();
|
||||
return if parts.len() < 2{
|
||||
None
|
||||
} else{
|
||||
let hostname: String = (*parts.get(1).unwrap()).into();
|
||||
Some(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from(s: String) -> Command {
|
||||
let s = s.trim();
|
||||
if s.starts_with("/chan") {
|
||||
match Command::parse_chan_id(s.as_ref()) {
|
||||
Some(id) => Command::Channel(id),
|
||||
None => Command::Failure("no valid id(u64) provided")
|
||||
}
|
||||
} else if s.starts_with("/serv") {
|
||||
match Command::parse_hostname(s.as_ref()) {
|
||||
Some(hostname) => Command::Server(hostname),
|
||||
None => Command::Failure("no hostname provided")
|
||||
}
|
||||
} else if s.starts_with("/help") {
|
||||
Command::Help
|
||||
}
|
||||
else {
|
||||
if s.starts_with("/") {
|
||||
Command::Failure("command not found")
|
||||
} else {
|
||||
Command::Message(s.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn styled(&self) -> Spans {
|
||||
use Command::*;
|
||||
return match self {
|
||||
Help => Spans::from(vec![
|
||||
Span::styled("! /help\n", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::styled("! /channel <u64>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::styled("! /server <u64>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
Channel(id) => Spans::from(vec![
|
||||
Span::styled(format!("! /channel "), Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(format!("{}", id)),
|
||||
]),
|
||||
Server(hostname) => Spans::from(vec![
|
||||
Span::styled("! /server ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(hostname)
|
||||
]),
|
||||
Message(msg) => Spans::from(vec![
|
||||
Span::styled("(You) ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(msg)
|
||||
]),
|
||||
Failure(msg) => Spans::from(vec![
|
||||
Span::styled("! error ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(*msg)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
72
tui/src/config.rs
Normal file
72
tui/src/config.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ServerMeta {
|
||||
pub protocol: String,
|
||||
pub hostname: String,
|
||||
pub port: Option<u16>,
|
||||
pub description: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ServerConfig {
|
||||
pub user: UserConfig,
|
||||
pub server: ServerMeta
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UserConfig {
|
||||
pub id: u64,
|
||||
pub secret: String,
|
||||
pub jwt: Option<String>,
|
||||
|
||||
pub permissions: u64,
|
||||
pub joindate: i64,
|
||||
pub status: i32,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub username: Option<String>,
|
||||
pub servers: Vec<ServerConfig>
|
||||
}
|
||||
|
||||
|
||||
impl Config {
|
||||
pub fn update_jwt(&mut self, hostname: String, jwt: String) {
|
||||
for servermeta in self.servers.iter_mut() {
|
||||
if servermeta.server.hostname == hostname {
|
||||
servermeta.user.jwt = Some(jwt);
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_url(&self, hostname: &str) -> Option<String> {
|
||||
// Finds the base-url for the given hostname, assuming it exists
|
||||
let mut url: Option<String> = None;
|
||||
for meta in self.servers.iter() {
|
||||
if meta.server.hostname == hostname {
|
||||
url = match meta.server.port {
|
||||
Some(p) => Some(format!("{}://{}:{}", meta.server.protocol, hostname, p)),
|
||||
None => Some(format!("{}://{}", meta.server.protocol, hostname))
|
||||
};
|
||||
break
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
pub fn save(&self, path: String) -> std::io::Result<()>{
|
||||
use std::io::prelude::Write;
|
||||
use std::fs::File;
|
||||
|
||||
let content = serde_json::to_string_pretty(self)?;
|
||||
let mut file = File::create(path)?;
|
||||
file.write(content.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
use cursive::Cursive;
|
||||
use reqwest;
|
||||
use crate::types::Channel;
|
||||
|
||||
pub async fn fetch_channels(domain: &str, port: u16) -> Option<Vec<Channel>>{
|
||||
let url = format!("http://{}:{}/channels/list", domain, port);
|
||||
|
||||
if let Ok(resp) = reqwest::get(&url).await {
|
||||
let bytes = resp.bytes().await.unwrap();
|
||||
let res: Result<Vec<Channel>, serde_json::Error> = serde_json::from_slice(&bytes);
|
||||
return match res {
|
||||
Ok(res) => Some(res),
|
||||
_ => None
|
||||
};
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
pub mod sync {
|
||||
use cursive::Cursive;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
pub fn open_channel(ip: Ipv4Addr, name: &str, s: &mut Cursive) {
|
||||
}
|
||||
|
||||
}
|
259
tui/src/main.rs
259
tui/src/main.rs
@ -1,99 +1,224 @@
|
||||
extern crate serde;
|
||||
extern crate clap;
|
||||
extern crate cursive;
|
||||
mod util;
|
||||
mod command;
|
||||
mod config;
|
||||
mod api_types;
|
||||
mod cache;
|
||||
|
||||
extern crate tokio;
|
||||
extern crate reqwest;
|
||||
use crate::util::event::{Event, Events};
|
||||
use crate::command::Command;
|
||||
use crate::cache::Cache;
|
||||
use std::{env, fs, error::Error, io};
|
||||
use clap::{App as Clap, Arg, ArgMatches};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Wrap, Block, Borders, Paragraph},
|
||||
Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use std::{fs, env};
|
||||
use std::path::PathBuf;
|
||||
enum InputMode {
|
||||
Normal,
|
||||
Editing,
|
||||
}
|
||||
|
||||
use clap::{Arg, App};
|
||||
use cursive::Cursive;
|
||||
use cursive::menu::MenuTree;
|
||||
use cursive::event::Key;
|
||||
/// App holds the state of the application
|
||||
struct App {
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
messages: Vec<Command>,
|
||||
cache: Cache
|
||||
}
|
||||
|
||||
use serde_json;
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
cache: Cache::default() // empty cache lad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod types;
|
||||
mod http;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = App::new("Freechat TUI")
|
||||
.version("69.420")
|
||||
.author("godrah")
|
||||
.about("oh you know")
|
||||
fn get_args() -> ArgMatches<'static> {
|
||||
Clap::new("Freechat TUI")
|
||||
.version("v69.420")
|
||||
.author("Author: shockrah")
|
||||
.about("Description: A terminal based program for interacting with freechat servers")
|
||||
.arg(Arg::with_name("config")
|
||||
.short("c")
|
||||
.long("config")
|
||||
.value_name("CONFIG")
|
||||
.help("Specify path of config to use")
|
||||
.takes_value(true))
|
||||
.arg(Arg::with_name("theme")
|
||||
.short("t")
|
||||
.long("theme")
|
||||
.value_name("THEME_FILE")
|
||||
.help("Specify theme file on startup")
|
||||
.takes_value(true)).get_matches();
|
||||
|
||||
let config: types::Config = if args.args.len() == 0 {
|
||||
.arg(Arg::with_name("no_login")
|
||||
.short("l")
|
||||
.long("no-login")
|
||||
.help("Don't automatically login on startup")).get_matches()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
// parameter things first
|
||||
let args = get_args();
|
||||
|
||||
let (config_path, mut config): (String, config::Config) = if args.args.len() == 0 {
|
||||
let home = env::var("HOME").unwrap();
|
||||
match fs::read_to_string(format!("{}/.config/freechat/config.json", home)) {
|
||||
Ok(data) => serde_json::from_str(&data).unwrap(),
|
||||
Err(e) => panic!("Bro: {}", e)
|
||||
let path = format!("{}/.config/freechat/config.json", home);
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(data) => (path, serde_json::from_str(&data).unwrap()),
|
||||
Err(e) => panic!("Unable to parse config file @{} : {}", path, e)
|
||||
}
|
||||
} else{
|
||||
let path = args.value_of("config").unwrap();
|
||||
match fs::read_to_string(path) {
|
||||
Ok(data) => serde_json::from_str(&data).unwrap(),
|
||||
Err(e) => panic!("Bro: {}", e)
|
||||
Ok(data) => (path.into(), serde_json::from_str(&data).unwrap()),
|
||||
Err(e) => panic!("Unable to parse config @ {} : {}", path, e)
|
||||
}
|
||||
};
|
||||
|
||||
// Terminal initialization
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let stdout = AlternateScreen::from(stdout);
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// only load a theme if requested
|
||||
let theme = if let Some(theme) = args.value_of("theme") {
|
||||
Some(PathBuf::from(theme))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Setup event handlers
|
||||
let mut events = Events::new();
|
||||
|
||||
let mut app = cursive::default();
|
||||
// Create default app state
|
||||
let mut app = App::default();
|
||||
|
||||
// optionally load optional theme
|
||||
if let Some(theme) = theme { let _ = app.load_theme_file(theme); }
|
||||
loop {
|
||||
// Draw UI
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
app.add_global_callback('q', Cursive::quit);
|
||||
app.add_global_callback(Key::Esc, |s| s.select_menubar());
|
||||
app.set_autohide_menu(false); // don't hide the menubar all the time
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
Span::styled("I", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" for insert mode"),
|
||||
],
|
||||
Style::default()
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
Span::raw("Keys: "),
|
||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::styled(" Enter ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::styled("/help", Style::default().add_modifier(Modifier::BOLD))
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Spans::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
let input = Paragraph::new(app.input.as_ref())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(input, chunks[1]);
|
||||
|
||||
let mut lines: Vec<Spans> = Vec::new();
|
||||
for cmd in app.messages.iter() {
|
||||
&lines.push(cmd.styled());
|
||||
}
|
||||
|
||||
// menu bar at the top lets us pick between different servers in the config
|
||||
for server in config.servers.iter() {
|
||||
let name = match &server.name {
|
||||
Some(name) => name.to_string(),
|
||||
//None => String::from(&format!("{}", server.ip))
|
||||
None => String::from("None")
|
||||
};
|
||||
let list = Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.block(Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Messages"));
|
||||
|
||||
app.menubar().add_subtree(&name, MenuTree::new()); // add server name
|
||||
// on action:
|
||||
// open up search able list of channels
|
||||
// choose from that list of channels which one you want to see
|
||||
f.render_widget(list, chunks[2]);
|
||||
|
||||
// NOTE: not passing the domain as the IP is resolved on server join
|
||||
if let Some(channels) = http::fetch_channels(&server.ip, server.port).await {
|
||||
// add a bunch of actionable leafs to our sub tree
|
||||
for channel in channels {
|
||||
app.menubar().find_subtree(name.as_ref()).unwrap().add_leaf(channel.name.clone(), move |s| {
|
||||
let (ip, name) = channel.parts();
|
||||
http::sync::open_channel(ip, name, s);
|
||||
});
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
||||
f.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
chunks[1].x + app.input.width() as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
// Handle input
|
||||
if let Event::Input(input) = events.next()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match input {
|
||||
Key::Char('i') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
events.disable_exit_key();
|
||||
}
|
||||
Key::Char('q') => {
|
||||
// save the config as is then quit out
|
||||
match config.save(config_path) {
|
||||
Ok(_) => println!(":^)"),
|
||||
Err(e) => eprintln!("Couldn't save config state: {}", e)
|
||||
};
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => match input {
|
||||
Key::Char('\n') => {
|
||||
let cmd = Command::from(app.input.drain(..).collect());
|
||||
app.messages.push(match cmd {
|
||||
// only for networked commands do we need to touch cache
|
||||
Command::Channel(id) => app.cache.switch_channel(id).await,
|
||||
Command::Server(host) => app.cache.switch_server(&host).await,
|
||||
Command::Message(msg) => app.cache.send_message(&msg).await,
|
||||
_ => cmd
|
||||
});
|
||||
}
|
||||
Key::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
Key::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
Key::Ctrl('c') | Key::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
events.enable_exit_key();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.run();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
use serde::{Serialize,Deserialize};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Server {
|
||||
pub name: Option<String>,
|
||||
pub domain: Option<String>,
|
||||
pub ip: String,
|
||||
pub port: u16,
|
||||
pub description: Option<String>,
|
||||
pub key: String, // the secret hush hush uwu
|
||||
pub id: u64,
|
||||
pub nickname: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub username: String, // global username that is only overriden in server context's if nickname is used
|
||||
pub servers: Vec<Server>
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Channel {
|
||||
pub ip: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn parts(&self) -> (Ipv4Addr, &str) {
|
||||
// return the ip/name of the channel
|
||||
let addr = Ipv4Addr::from_str(&self.ip).unwrap();
|
||||
|
||||
(addr, self.name.as_ref())
|
||||
}
|
||||
}
|
95
tui/src/util/event.rs
Normal file
95
tui/src/util/event.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
|
||||
pub enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// A small event handler that wrap termion input and tick events. Each event
|
||||
/// type is handled in its own thread and returned to a common `Receiver`
|
||||
pub struct Events {
|
||||
rx: mpsc::Receiver<Event<Key>>,
|
||||
input_handle: thread::JoinHandle<()>,
|
||||
ignore_exit_key: Arc<AtomicBool>,
|
||||
tick_handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Config {
|
||||
pub exit_key: Key,
|
||||
pub tick_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
exit_key: Key::Char('q'),
|
||||
tick_rate: Duration::from_millis(250),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Events {
|
||||
pub fn new() -> Events {
|
||||
Events::with_config(Config::default())
|
||||
}
|
||||
|
||||
pub fn with_config(config: Config) -> Events {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let ignore_exit_key = Arc::new(AtomicBool::new(false));
|
||||
let input_handle = {
|
||||
let tx = tx.clone();
|
||||
let ignore_exit_key = ignore_exit_key.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for evt in stdin.keys() {
|
||||
if let Ok(key) = evt {
|
||||
if let Err(err) = tx.send(Event::Input(key)) {
|
||||
eprintln!("{}", err);
|
||||
return;
|
||||
}
|
||||
if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let tick_handle = {
|
||||
thread::spawn(move || loop {
|
||||
if tx.send(Event::Tick).is_err() {
|
||||
break;
|
||||
}
|
||||
thread::sleep(config.tick_rate);
|
||||
})
|
||||
};
|
||||
Events {
|
||||
rx,
|
||||
ignore_exit_key,
|
||||
input_handle,
|
||||
tick_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
}
|
||||
|
||||
pub fn disable_exit_key(&mut self) {
|
||||
self.ignore_exit_key.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn enable_exit_key(&mut self) {
|
||||
self.ignore_exit_key.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
21
tui/src/util/mod.rs
Normal file
21
tui/src/util/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
//#[cfg(feature = "termion")]
|
||||
pub mod event;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
shadow = true
|
||||
borders = "outset"
|
||||
|
||||
[colors]
|
||||
shadow = "#1a1a1a"
|
||||
background = "black"
|
||||
primary = ["black"]
|
||||
secondary = "black"
|
||||
tertiary = "black"
|
||||
title_primary = "#3560e0"
|
||||
title_secondary = "#ffff55"
|
||||
highlight = "#3560e0"
|
||||
highlight_inactive = "#7a93c9"
|
||||
|
@ -1,13 +0,0 @@
|
||||
shadow = false
|
||||
borders = "outset"
|
||||
|
||||
[colors]
|
||||
background = "black"
|
||||
primary = ["#000000"]
|
||||
secondary = "black"
|
||||
tertiary = "#000000"
|
||||
title_primary = "black"
|
||||
title_secondary = "#000000"
|
||||
highlight = "#000000"
|
||||
highlight_inactive = "#7a7a7a"
|
||||
|
@ -1,11 +0,0 @@
|
||||
[colors]
|
||||
background = "white"
|
||||
primary = "white"
|
||||
secondary = "white"
|
||||
tertiary = "white"
|
||||
title_primary = "white"
|
||||
title_secondary = "white"
|
||||
highlight = "white"
|
||||
highlight_inactive = "#e0e0e0"
|
||||
view = "black"
|
||||
|
Loading…
Reference in New Issue
Block a user