mod generic_backend;
mod key;
mod keyboard100;
mod keyboard60;
mod keyboard80;
mod menu;
mod model;
mod view;
use std::{
collections::HashMap,
io,
sync::mpsc::{channel, Receiver, Sender},
thread,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use generic_backend::GenericKeyBackend;
use model::*;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::Rect,
Terminal,
};
use view::draw_too_small;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version)]
struct Args {}
pub const KEY_HEIGHT: u16 = 3;
fn main() -> Result<(), KbtError> {
log::info!("start the app!");
let _ = Args::parse();
run().map(|_| println!("bye!"))
}
fn run() -> Result<(), KbtError> {
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
enable_raw_mode()?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let menu_result: MenuResult = menu::run_menu(&mut terminal)?;
match menu_result {
MenuResult::Terminate => Ok(()),
MenuResult::KeyboardSelected(selection) => {
let (sender, receiver): (Sender<AppEvent>, Receiver<AppEvent>) = channel();
let (_up_guard, _down_guard) = GenericKeyBackend::subscribe(&sender);
let handle = thread::spawn(move || listen_for_control(sender));
let layout = match &selection {
KeyboardSize::Keyboard60 => {
prepare_layout(keyboard60::ROWS.map(|rows| rows.to_vec()).to_vec())
}
KeyboardSize::Keyboard80 => {
prepare_layout(keyboard80::ROWS.map(|rows| rows.to_vec()).to_vec())
}
KeyboardSize::Keyboard100 => {
prepare_layout(keyboard100::ROWS.map(|rows| rows.to_vec()).to_vec())
}
};
let initial_app = App {
key_states: HashMap::new(),
event_receiver: receiver,
layout,
};
let res = run_keyboard(&mut terminal, initial_app);
match handle.join() {
Ok(_) => res,
Err(_) => Err(KbtError {
message: String::from("Control listener thread failed to exit"),
}),
}
}
}?;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn prepare_layout(rows: Vec<Vec<KeyUI>>) -> KeyboardLayout {
let rows_count: u16 = u16::try_from(rows.len()).unwrap_or(0);
let height: u16 = rows_count * KEY_HEIGHT;
let width: u16 = rows
.iter()
.map(|row| row.iter().map(|key| key.size.static_len()).sum())
.max()
.unwrap_or(0);
let rows: Vec<Row> = rows
.iter()
.map(|row| Row {
width: calc_row_width(row),
keys: row.to_vec(),
})
.collect();
KeyboardLayout {
rows,
height,
width,
rows_count,
}
}
fn calc_row_width(row_keys: &[KeyUI]) -> u16 {
row_keys
.iter()
.map(|key| (key.size.static_len() as i16 + key.size_correction.unwrap_or(0)) as u16)
.sum()
}
fn listen_for_control(sender: Sender<AppEvent>) -> Result<(), KbtError> {
loop {
match event::read()? {
Event::Key(key) => match key.code {
KeyCode::Char('c') | KeyCode::Char('q') => {
if key.modifiers == KeyModifiers::CONTROL {
sender.send(AppEvent::ControlEvent(ControlEventType::Terminate))?;
return Ok(());
}
}
KeyCode::Char('r') => {
if key.modifiers == KeyModifiers::CONTROL {
sender.send(AppEvent::ControlEvent(ControlEventType::Reset))?;
}
}
_ => {}
},
Event::Resize(_, _) => sender.send(AppEvent::ScreenResize)?,
_ => {}
}
}
}
fn run_keyboard<B: Backend>(terminal: &mut Terminal<B>, mut state: App) -> Result<(), KbtError> {
_ = state
.event_receiver
.recv_timeout(Duration::from_millis(100));
loop {
if layout_fits(terminal.size()?, &state) {
terminal.draw(|f| view::draw(f, &state).expect("Failed to draw"))?
} else {
terminal.draw(draw_too_small)?
};
let app_event = state.event_receiver.recv()?;
match app_event {
AppEvent::KeyEvent(KeyEventType::KeyPressed(key)) => {
state.key_states.insert(key, KeyState::Pressed);
}
AppEvent::KeyEvent(KeyEventType::KeyReleased(key)) => {
if let Some(KeyState::Pressed) = state.key_states.get(&key) {
state.key_states.insert(key, KeyState::Released);
{}
}
}
AppEvent::ControlEvent(control) => match control {
ControlEventType::Terminate => {
return Ok(());
}
ControlEventType::Reset => {
state.reset();
}
},
AppEvent::ScreenResize => {}
}
}
}
fn layout_fits(terminal_size: Rect, state: &App) -> bool {
terminal_size.width > state.layout.width && terminal_size.height > state.layout.height
}