1
0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2026-01-22 02:10:24 +00:00

Quick Menu + Ryujinx Compatibility (#313)

* Initial commit

* Format Rust code using rustfmt

* Add back fs calls

* working with drawing

* wow we're almost there

* multi-lists working, selection within tui working

* working with tabs

* working with smash

* amend warnings, fix menu actually saving inputs

* small refactors

* Fully working!

* Fix warnings

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
jugeeya
2022-03-20 11:09:25 -07:00
committed by GitHub
parent c6c4105fc3
commit e4e2de0a79
17 changed files with 1916 additions and 774 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "training_mod_tui"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tui = { version = "0.16.0", default-features = false }
unicode-width = "0.1.9"
training_mod_consts = { path = "../training_mod_consts", default-features = false}
serde_json = "1.0.79"
bitflags = "1.2.1"
crossterm = { version = "0.22.1", optional = true }
[features]
default = []
has_terminal = ["crossterm", "tui/crossterm"]

454
training_mod_tui/src/lib.rs Normal file
View File

@@ -0,0 +1,454 @@
use training_mod_consts::{OnOffSelector, Slider, SubMenu, SubMenuType, Toggle};
use tui::{
backend::{Backend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
text::Spans,
widgets::{Tabs, Paragraph, Block, List, ListItem, ListState},
Frame,
};
pub use tui::{backend::TestBackend, Terminal};
use std::collections::HashMap;
mod list;
use crate::list::{StatefulList, MultiStatefulList};
/// We should hold a list of SubMenus.
/// The currently selected SubMenu should also have an associated list with necessary information.
/// We can convert the option types (Toggle, OnOff, Slider) to lists
pub struct App<'a> {
pub tabs: StatefulList<&'a str>,
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
pub selected_sub_menu_onoff_selectors: MultiStatefulList<OnOffSelector<'a>>,
pub selected_sub_menu_sliders: MultiStatefulList<Slider>,
pub outer_list: bool
}
impl<'a> App<'a> {
pub fn new(menu: training_mod_consts::Menu<'a>) -> App<'a> {
let tab_specifiers = vec![
("Mash Settings", vec![
"Mash Toggles",
"Followup Toggles",
"Attack Angle",
"Ledge Options",
"Ledge Delay",
"Tech Options",
"Miss Tech Options",
"Defensive Options",
"Aerial Delay",
"OoS Offset",
"Reaction Time",
]),
("Defensive Settings", vec![
"Fast Fall",
"Fast Fall Delay",
"Falling Aerials",
"Full Hop",
"Shield Tilt",
"DI Direction",
"SDI Direction",
"Airdodge Direction",
"SDI Strength",
"Shield Toggles",
"Mirroring",
"Throw Options",
"Throw Delay",
"Pummel Delay",
"Buff Options",
]),
("Other Settings", vec![
"Input Delay",
"Save States",
"Save Damage",
"Hitbox Visualization",
"Stage Hazards",
"Frame Advantage",
"Mash In Neutral",
"Quick Menu"
])
];
let mut tabs: std::collections::HashMap<&str, Vec<SubMenu>> = std::collections::HashMap::new();
tabs.insert("Mash Settings", vec![]);
tabs.insert("Defensive Settings", vec![]);
tabs.insert("Other Settings", vec![]);
for sub_menu in menu.sub_menus.iter() {
for tab_spec in tab_specifiers.iter() {
if tab_spec.1.contains(&sub_menu.title) {
tabs.get_mut(tab_spec.0).unwrap().push(sub_menu.clone());
}
}
};
let num_lists = 3;
let mut menu_items_stateful = HashMap::new();
tabs.keys().for_each(|k| {
menu_items_stateful.insert(
k.clone(),
MultiStatefulList::with_items(tabs.get(k).unwrap().clone(), num_lists)
);
});
let mut app = App {
tabs: StatefulList::with_items(tab_specifiers.iter().map(|(tab_title, _)| *tab_title).collect()),
menu_items: menu_items_stateful,
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_onoff_selectors: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0),
outer_list: true
};
app.set_sub_menu_items();
app
}
pub fn set_sub_menu_items(&mut self) {
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap();
let toggles = selected_sub_menu.toggles.clone();
let sliders = selected_sub_menu.sliders.clone();
let onoffs = selected_sub_menu.onoffselector.clone();
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => {
self.selected_sub_menu_toggles = MultiStatefulList::with_items(
toggles,
if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} )
},
SubMenuType::SLIDER => {
self.selected_sub_menu_sliders = MultiStatefulList::with_items(
sliders,
if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} )
},
SubMenuType::ONOFF => {
self.selected_sub_menu_onoff_selectors = MultiStatefulList::with_items(
onoffs,
if selected_sub_menu.onoffselector.len() >= 3 { 3 } else { selected_sub_menu.onoffselector.len()} )
},
};
}
fn tab_selected(&self) -> &str {
self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()
}
fn sub_menu_selected(&self) -> &SubMenu {
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
&self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap()
}
pub fn sub_menu_next(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next(),
}
}
pub fn sub_menu_next_list(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next_list(),
}
}
pub fn sub_menu_previous(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous(),
}
}
pub fn sub_menu_previous_list(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous_list(),
}
}
pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(&str, &str)>, ListState)>) {
(self.sub_menu_selected().title, self.sub_menu_selected().help_text,
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => {
self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| {
(toggle_list.items.iter().map(
|toggle| (toggle.checked, toggle.title)
).collect(), toggle_list.state.clone())
}).collect()
},
SubMenuType::SLIDER => {
vec![(vec![], ListState::default())]
},
SubMenuType::ONOFF => {
self.selected_sub_menu_onoff_selectors.lists.iter().map(|onoff_list| {
(onoff_list.items.iter().map(
|onoff| (onoff.checked, onoff.title)
).collect(), onoff_list.state.clone())
}).collect()
},
})
}
pub fn on_a(&mut self) {
if self.outer_list {
self.outer_list = false;
} else {
let tab_selected = self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap();
let (list_section, list_idx) = self.menu_items.get(tab_selected)
.unwrap()
.idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
let selected_sub_menu = self.menu_items.get_mut(tab_selected)
.unwrap()
.lists[list_section]
.items.get_mut(list_idx).unwrap();
match SubMenuType::from_str(selected_sub_menu._type) {
SubMenuType::TOGGLE => {
let is_single_option = selected_sub_menu.is_single_option.is_some();
let state = self.selected_sub_menu_toggles.state;
self.selected_sub_menu_toggles.lists.iter_mut()
.map(|list| (list.state.selected(), &mut list.items))
.for_each(|(state, toggle_list)| toggle_list.iter_mut()
.enumerate()
.for_each(|(i, o)|
if state.is_some() && i == state.unwrap() {
if o.checked != "is-appear" {
o.checked = "is-appear";
} else {
o.checked = "is-hidden";
}
} else if is_single_option {
o.checked = "is-hidden";
}
));
selected_sub_menu.toggles.iter_mut()
.enumerate()
.for_each(|(i, o)| {
if i == state {
if o.checked != "is-appear" {
o.checked = "is-appear";
} else {
o.checked = "is-hidden";
}
} else if is_single_option {
o.checked = "is-hidden";
}
});
},
SubMenuType::ONOFF => {
let onoff = self.selected_sub_menu_onoff_selectors.selected_list_item();
if onoff.checked != "is-appear" {
onoff.checked = "is-appear";
} else {
onoff.checked = "is-hidden";
}
selected_sub_menu.onoffselector.iter_mut()
.filter(|o| o.title == onoff.title)
.for_each(|o| o.checked = onoff.checked);
},
SubMenuType::SLIDER => {
// self.selected_sub_menu_sliders.selected_list_item().checked = "is-appear";
}
}
}
}
pub fn on_b(&mut self) {
self.outer_list = true;
}
pub fn on_l(&mut self) {
if self.outer_list {
self.tabs.previous();
self.set_sub_menu_items();
}
}
pub fn on_r(&mut self) {
if self.outer_list {
self.tabs.next();
self.set_sub_menu_items();
}
}
pub fn on_up(&mut self) {
if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous();
self.set_sub_menu_items();
} else {
self.sub_menu_previous();
}
}
pub fn on_down(&mut self) {
if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next();
self.set_sub_menu_items();
} else {
self.sub_menu_next();
}
}
pub fn on_left(&mut self) {
if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().previous_list();
self.set_sub_menu_items();
} else {
self.sub_menu_previous_list();
}
}
pub fn on_right(&mut self) {
if self.outer_list {
self.menu_items.get_mut(self.tabs.items.get(self.tabs.state.selected().unwrap()).unwrap()).unwrap().next_list();
self.set_sub_menu_items();
} else {
self.sub_menu_next_list();
}
}
}
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
let app_tabs = &app.tabs;
let tab_selected = app_tabs.state.selected().unwrap();
let titles = app_tabs.items.iter().cloned().enumerate().map(|(idx, tab)|{
if idx == tab_selected {
Spans::from(">> ".to_owned() + tab)
} else {
Spans::from(" ".to_owned() + tab)
}
}).collect();
let tabs = Tabs::new(titles)
.block(Block::default().title("Ultimate Training Modpack Menu"))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow))
.divider("|")
.select(tab_selected);
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2),
Constraint::Max(10),
Constraint::Length(2)].as_ref())
.split(f.size());
let list_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(33), Constraint::Percentage(32), Constraint::Percentage(33)].as_ref())
.split(vertical_chunks[1]);
f.render_widget(tabs, vertical_chunks[0]);
if app.outer_list {
let tab_selected = app.tab_selected();
let mut item_help = None;
for list_section in 0..app.menu_items.get(tab_selected).unwrap().lists.len() {
let stateful_list = &app.menu_items.get(tab_selected).unwrap().lists[list_section];
let items: Vec<ListItem> = stateful_list
.items
.iter()
.map(|i| {
let lines = vec![Spans::from(
if stateful_list.state.selected().is_some() {
i.title.to_owned()
} else {
" ".to_owned() + i.title
})];
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
.collect();
let list = List::new(items)
.block(Block::default().title(if list_section == 0 { "Options" } else { "" }))
.highlight_style(
Style::default()
.bg(Color::LightBlue)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
let mut state = stateful_list.state.clone();
if state.selected().is_some() {
item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
}
f.render_stateful_widget(list, list_chunks[list_section], &mut state);
}
let help_paragraph = Paragraph::new(
item_help.unwrap_or("").replace("\"", "") +
"\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab | R: Save defaults"
);
f.render_widget(help_paragraph, vertical_chunks[2]);
} else {
let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
for list_section in 0..sub_menu_str_lists.len() {
let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
let values_items: Vec<ListItem> = sub_menu_str.iter().map(|s| {
ListItem::new(
vec![
Spans::from((if s.0 == "is-appear" { "X " } else { " " }).to_owned() + s.1)
]
)
}).collect();
let values_list = List::new(values_items)
.block(Block::default().title(if list_section == 0 { title } else { "" }))
.start_corner(Corner::TopLeft)
.highlight_style(
Style::default()
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
}
let help_paragraph = Paragraph::new(
help_text.replace("\"", "") +
"\nA: Select toggle | B: Exit submenu"
);
f.render_widget(help_paragraph, vertical_chunks[2]);
}
let mut url = "http://localhost/".to_owned();
let mut settings = HashMap::new();
// Collect settings for toggles
for key in app.menu_items.keys() {
for list in &app.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items {
let mut val = String::new();
sub_menu.toggles.iter()
.filter(|t| t.checked == "is-appear")
.for_each(|t| val.push_str(format!("{},", t.value).as_str()));
sub_menu.onoffselector.iter()
.for_each(|o| {
val.push_str(
format!("{}", if o.checked == "is-appear" { 1 } else { 0 }).as_str())
});
settings.insert(sub_menu.id, val);
}
}
}
url.push_str("?");
settings.iter()
.for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str()));
url
// TODO: Add saveDefaults
// if (document.getElementById("saveDefaults").checked) {
// url += "save_defaults=1";
// } else {
// url = url.slice(0, -1);
// }
}

View File

@@ -0,0 +1,191 @@
use tui::widgets::ListState;
pub struct MultiStatefulList<T> {
pub lists: Vec<StatefulList<T>>,
pub state: usize,
pub total_len: usize
}
impl<T: Clone> MultiStatefulList<T> {
pub fn selected_list_item(&mut self) -> &mut T {
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
&mut self
.lists[list_section]
.items[list_idx]
}
pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) {
for list_section in 0..self.lists.len() {
let list_section_min_idx = (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section;
let list_section_max_idx = std::cmp::min(
(self.total_len as f32 / self.lists.len() as f32).ceil() as usize * (list_section + 1),
self.total_len);
if (list_section_min_idx..list_section_max_idx).contains(&idx) {
// println!("\n{}: ({}, {})", idx, list_section_min_idx, list_section_max_idx);
return (list_section, idx - list_section_min_idx)
}
}
(0, 0)
}
fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize {
let list_section = list_idx.0;
let mut list_idx = list_idx.1;
for list_section in 0..list_section {
list_idx += self.lists[list_section].items.len();
}
list_idx
}
pub fn with_items(items: Vec<T>, num_lists: usize) -> MultiStatefulList<T> {
let lists = (0..num_lists).map(|list_section| {
let list_section_min_idx = (items.len() as f32 / num_lists as f32).ceil() as usize * list_section;
let list_section_max_idx = std::cmp::min(
(items.len() as f32 / num_lists as f32).ceil() as usize * (list_section + 1),
items.len());
let mut state = ListState::default();
if list_section == 0 {
// Enforce state as first of list
state.select(Some(0));
}
StatefulList {
state: state,
items: items[list_section_min_idx..list_section_max_idx].to_vec(),
}
}).collect();
let total_len = items.len();
MultiStatefulList {
lists: lists,
total_len: total_len,
state: 0
}
}
pub fn next(&mut self) {
let (list_section, _) = self.idx_to_list_idx(self.state);
let (next_list_section, next_list_idx) = self.idx_to_list_idx(self.state+1);
if list_section != next_list_section {
self.lists[list_section].unselect();
}
let state;
if self.state + 1 >= self.total_len {
state = (0, 0);
} else {
state = (next_list_section, next_list_idx);
}
self.lists[state.0].state.select(Some(state.1));
self.state = self.list_idx_to_idx(state);
}
pub fn previous(&mut self) {
let (list_section, _) = self.idx_to_list_idx(self.state);
let (last_list_section, last_list_idx) = (self.lists.len() - 1, self.lists[self.lists.len() - 1].items.len() - 1);
self.lists[list_section].unselect();
let state;
if self.state == 0 {
state = (last_list_section, last_list_idx);
} else {
let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1);
state = (prev_list_section, prev_list_idx);
}
self.lists[state.0].state.select(Some(state.1));
self.state = self.list_idx_to_idx(state);
}
pub fn next_list(&mut self) {
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
let next_list_section = (list_section + 1) % self.lists.len();
let next_list_idx;
if list_idx > self.lists[next_list_section].items.len() - 1 {
next_list_idx = self.lists[next_list_section].items.len() - 1;
} else {
next_list_idx = list_idx;
}
if list_section != next_list_section {
self.lists[list_section].unselect();
}
let state = (next_list_section, next_list_idx);
self.lists[state.0].state.select(Some(state.1));
self.state = self.list_idx_to_idx(state);
}
pub fn previous_list(&mut self) {
let (list_section, list_idx) = self.idx_to_list_idx(self.state);
let prev_list_section;
if list_section == 0 {
prev_list_section = self.lists.len() - 1;
} else {
prev_list_section = list_section - 1;
}
let prev_list_idx;
if list_idx > self.lists[prev_list_section].items.len() - 1 {
prev_list_idx = self.lists[prev_list_section].items.len() - 1;
} else {
prev_list_idx = list_idx;
}
if list_section != prev_list_section {
self.lists[list_section].unselect();
}
let state = (prev_list_section, prev_list_idx);
self.lists[state.0].state.select(Some(state.1));
self.state = self.list_idx_to_idx(state);
}
}
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
let mut state = ListState::default();
// Enforce state as first of list
state.select(Some(0));
StatefulList {
state: state,
items,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn unselect(&mut self) {
self.state.select(None);
}
}

View File

@@ -0,0 +1,113 @@
#[cfg(feature = "has_terminal")]
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
#[cfg(feature = "has_terminal")]
use tui::backend::CrosstermBackend;
#[cfg(feature = "has_terminal")]
use std::{
io,
time::{Duration, Instant},
};
use std::error::Error;
use tui::Terminal;
use training_mod_consts::*;
fn main() -> Result<(), Box<dyn Error>> {
let menu;
unsafe {
menu = get_menu();
}
#[cfg(not(feature = "has_terminal"))] {
let mut app = training_mod_tui::App::new(menu);
let backend = tui::backend::TestBackend::new(75, 15);
let mut terminal = Terminal::new(backend)?;
let mut state = tui::widgets::ListState::default();
state.select(Some(1));
let mut url = String::new();
let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?;
for (i, cell) in frame_res.buffer.content().into_iter().enumerate() {
print!("{}", cell.symbol);
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
print!("\n");
}
}
println!();
println!("URL: {}", url);
}
#[cfg(feature = "has_terminal")] {
let app = training_mod_tui::App::new(menu);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let tick_rate = Duration::from_millis(250);
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
} else {
println!("URL: {}", res.as_ref().unwrap());
}
}
Ok(())
}
#[cfg(feature = "has_terminal")]
fn run_app<B: tui::backend::Backend>(
terminal: &mut Terminal<B>,
mut app: training_mod_tui::App,
tick_rate: Duration,
) -> io::Result<String> {
let mut last_tick = Instant::now();
let mut url = String::new();
loop {
terminal.draw(|f| url = training_mod_tui::ui(f, &mut app).clone())?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(url),
KeyCode::Char('r') => app.on_r(),
KeyCode::Char('l') => app.on_l(),
KeyCode::Left => app.on_left(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
KeyCode::Up => app.on_up(),
KeyCode::Enter => app.on_a(),
KeyCode::Backspace => app.on_b(),
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RUST_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>