freechat/tui/src/main.rs
shockrah cb5975f235 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
2021-03-17 20:39:42 -07:00

225 lines
7.7 KiB
Rust

mod util;
mod command;
mod config;
mod api_types;
mod cache;
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;
enum InputMode {
Normal,
Editing,
}
/// 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
}
impl Default for App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cache: Cache::default() // empty cache lad
}
}
}
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("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();
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) => (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)?;
// Setup event handlers
let mut events = Events::new();
// Create default app state
let mut app = App::default();
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());
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());
}
let list = Paragraph::new(lines)
.wrap(Wrap { trim: false })
.block(Block::default()
.borders(Borders::ALL)
.title("Messages"));
f.render_widget(list, chunks[2]);
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();
}
_ => {}
},
}
}
}
Ok(())
}