
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
225 lines
7.7 KiB
Rust
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(())
|
|
}
|
|
|
|
|