Started working on a TUI tester, has keyboard and gamepad input handlers

master
Wynd 2024-10-19 01:02:32 +03:00
parent 0aa7f846b1
commit 4b87e6c013
6 changed files with 1263 additions and 97 deletions

820
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,30 @@
cargo-features = ["codegen-backend"]
[package]
name = "gamepad-tester"
name = "gamo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lints.rust]
unsafe_code = { level = "forbid" }
[dependencies]
gilrs = { version = "0.10.4" }
ratatui = { version = "0.28.1" }
color-eyre = { version = "0.6.3" }
tracing = { version = "0.1.40" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[profile.dev]
codegen-backend = "cranelift"
opt-level = 0
lto = false
incremental = true
[profile.release]
opt-level = "z"
opt-level = 3
strip = true
lto = true
codegen-units = 1

110
logs/latest.log 100644
View File

@ -0,0 +1,110 @@
2024-10-18T22:01:48.001029Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.001150Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.474890Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.474947Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.495298Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.495345Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.618136Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.659732Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.659792Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.743791Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:48.743851Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:49.058470Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:49.058527Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:49.099680Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:49.099736Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:49.579664Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:49.579721Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:50.259909Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:50.259973Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:50.259992Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:50.657504Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)
2024-10-18T22:01:50.657561Z INFO src/gamepad_manager.rs:33: Selected gamepad changed to: Some(
GamepadId(
0,
),
)

216
src/app.rs 100644
View File

@ -0,0 +1,216 @@
use std::time::{Duration, Instant};
use color_eyre::{owo_colors::OwoColorize, Result};
use gilrs::{Gamepad, Gilrs};
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Flex, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Circle, Shape},
Block, BorderType, Gauge, Padding, Paragraph, Widget,
},
DefaultTerminal,
};
use Constraint::{Fill, Length, Min, Percentage};
use crate::gamepad_manager::GamepadManager;
pub struct App {
pub manager: GamepadManager,
state: AppState,
tick: u64,
}
#[derive(Default, PartialEq, Eq)]
pub enum AppState {
#[default]
Running,
Quitting,
}
impl App {
pub fn new(manager: GamepadManager) -> Self {
App {
manager,
state: AppState::Running,
tick: 0,
}
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(20);
let mut last_tick = Instant::now();
while self.state == AppState::Running {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
self.handle_events()?;
}
self.handle_gamepad_inputs()?;
if last_tick.elapsed() >= tick_rate {
self.update();
last_tick = Instant::now();
}
}
Ok(())
}
fn handle_gamepad_inputs(&mut self) -> Result<()> {
while let Some(gilrs::Event { id, .. }) = self.manager.gilrs.next_event() {
self.manager.set_active_gamepad(id);
}
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
if let ratatui::crossterm::event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('r') => {
self.manager.scan_gamepads();
return Ok(());
}
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => self.quit(),
_ => {}
}
}
}
Ok(())
}
fn update(&mut self) {
self.tick = self.tick.wrapping_add(1);
}
fn quit(&mut self) {
self.state = AppState::Quitting;
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("App Example Title")
.block(
Block::bordered()
.border_type(BorderType::Rounded)
.padding(Padding::top(1)),
)
.alignment(Alignment::Center)
.render(area, buf);
}
fn render_connected_gamepad(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new(format!(
"{} connected gamepads",
self.manager.connected_gamepads()
))
.block(
Block::bordered()
.border_type(BorderType::Rounded)
.padding(Padding::top(1)),
)
.alignment(Alignment::Center)
.bold()
.render(area, buf);
}
fn render_right_buttons(&self, area: Rect, buf: &mut Buffer) {
let north_button = self.create_action_button_ui(gilrs::Button::North);
let east_button = self.create_action_button_ui(gilrs::Button::East);
let south_button = self.create_action_button_ui(gilrs::Button::South);
let west_button = self.create_action_button_ui(gilrs::Button::West);
let layers = Layout::vertical([Fill(1), Fill(1), Fill(1)]);
let [top, mid, bot] = layers.areas(area);
let top_layer = Layout::horizontal([Fill(1), Fill(1), Fill(1)]);
let [_, north, _] = top_layer.areas(top);
let mid_layer = Layout::horizontal([Fill(1), Fill(1), Fill(1)]);
let [west, _, east] = mid_layer.areas(mid);
let bot_layer = Layout::horizontal([Fill(1), Fill(1), Fill(1)]);
let [_, south, _] = bot_layer.areas(bot);
north_button.render(north, buf);
east_button.render(east, buf);
south_button.render(south, buf);
west_button.render(west, buf);
}
fn create_action_button_ui(&self, dir: gilrs::Button) -> Block {
let color = match self.manager.active_gamepad() {
Ok(gamepad) => match gamepad.is_pressed(dir) {
true => Color::Green,
false => Color::Gray,
},
Err(_) => Color::Gray,
};
Block::new().style(Style::default().bg(color))
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Length(5), Min(0), Length(5)]);
let [header_area, inner_area, footer_area] = layout.areas(area);
let header_layout = Layout::horizontal([Fill(1), Fill(4)]);
let [tabs_area, title_area] = header_layout.areas(header_area);
let gamepad_layout = Layout::vertical([
Percentage(10),
Percentage(50),
Percentage(30),
Percentage(10),
]);
let [_, buttons, joysticks, _] = gamepad_layout.areas(inner_area);
let buttons_layout = Layout::horizontal([Fill(1), Fill(2), Fill(2), Fill(2), Fill(1)]);
let [_, left_buttons, mid_buttons, right_buttons, _] = buttons_layout.areas(buttons);
// let details_layout = Layout::horizontal([Fill(2), Fill(2)]);
// let [right_details_area, left_details_area] = details_layout.areas(details_area);
App::render_title(title_area, buf);
self.render_connected_gamepad(tabs_area, buf);
Block::bordered().render(inner_area, buf);
// Block::bordered()
// .style(Style::default().bg(tailwind::GREEN.c900))
// .render(right_details_area, buf);
// Block::new()
// .style(Style::default().bg(tailwind::YELLOW.c300))
// .render(details_area, buf);
self.render_right_buttons(right_buttons, buf);
// Block::bordered()
// .style(Style::default().bg(tailwind::TEAL.c300))
// .render(left_details_area, buf);
// let gauge_progress: u16 = self
// .tick
// .wrapping_div(100)
// .clamp(0, 100)
// .try_into()
// .unwrap();
// Gauge::default()
// .gauge_style(tailwind::BLUE.c900)
// .percent(gauge_progress)
// .render(right_details_area, buf);
// self.render_title(title_area, buf);
// self.render_tabs(tabs_area, buf);
// self.selected_tab.render(inner_area, buf);
// render_footer(footer_area, buf);
}
}

View File

@ -0,0 +1,98 @@
use color_eyre::{eyre::eyre, Result};
use gilrs::{
ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks},
Gamepad, GamepadId, Gilrs,
};
pub struct GamepadManager {
pub gilrs: Gilrs,
gamepads: Vec<GamepadId>,
active_gamepad: Option<GamepadId>,
connected_gamepads: usize,
}
impl GamepadManager {
pub fn new() -> Result<Self> {
let gilrs = Gilrs::new().map_err(|e| eyre!("Failed to create Gilrs object:\n{:#?}", e))?;
Ok(GamepadManager {
gilrs,
gamepads: vec![],
active_gamepad: None,
connected_gamepads: 0,
})
}
pub fn select_gamepad(&mut self, id: impl Into<Option<GamepadId>>) {
self.active_gamepad = id.into();
}
pub fn set_active_gamepad(&mut self, id: GamepadId) {
self.active_gamepad = Some(id);
tracing::info!("Selected gamepad changed to: {:#?}", self.active_gamepad);
}
pub fn active_gamepad(&self) -> Result<Gamepad<'_>> {
match self.active_gamepad {
Some(id) => Ok(self.gilrs.gamepad(id)),
None => Err(eyre!("No selected gamepad")),
}
}
pub fn gamepads(&self) -> &Vec<GamepadId> {
&self.gamepads
}
pub fn connected_gamepads(&self) -> &usize {
&self.connected_gamepads
}
pub fn scan_gamepads(&mut self) {
let mut gamepads = vec![];
// Iterate over all connected gamepads
for (id, gamepad) in self.gilrs.gamepads() {
// println!("{} is {:?}", gamepad.name(), gamepad.power_info());
// println!("Has Force Feedback Enabled ? {}", gamepad.is_ff_supported());
// if gamepad.is_ff_supported() {
// test_ff(&mut gilrs, gamepad);
// }
gamepads.push(id);
}
self.connected_gamepads = gamepads.len();
self.gamepads = gamepads;
}
pub fn test_ff(&mut self, gamepad_id: GamepadId) {
let gamepad = self.gilrs.gamepad(gamepad_id);
let duration: u32 = 1000;
let ff_play_ticks = Ticks::from_ms(duration);
let effect = EffectBuilder::new()
.add_effect(BaseEffect {
kind: BaseEffectType::Strong { magnitude: 40_000 },
scheduling: Replay {
play_for: ff_play_ticks,
with_delay: ff_play_ticks * 2,
..Default::default()
},
envelope: Default::default(),
})
.add_effect(BaseEffect {
kind: BaseEffectType::Weak { magnitude: 40_000 },
scheduling: Replay {
after: ff_play_ticks,
play_for: ff_play_ticks,
..Default::default()
},
envelope: Default::default(),
})
.add_gamepad(&gamepad)
.finish(&mut self.gilrs)
.unwrap();
effect.play().unwrap();
}
}

View File

@ -1,55 +1,65 @@
use std::{thread, time::Duration};
use gilrs::{
ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks},
Gamepad, Gilrs,
use std::{
fs::{File, OpenOptions},
thread,
time::Duration,
};
fn main() {
let mut gilrs = Gilrs::new().unwrap();
use app::{App, AppState};
use color_eyre::{eyre::eyre, Result};
use gamepad_manager::GamepadManager;
use tracing_subscriber::{layer::SubscriberExt, Layer};
let mut counter = 0;
mod app;
mod gamepad_manager;
// Iterate over all connected gamepads
for (_, gamepad) in Gilrs::new().unwrap().gamepads() {
println!("{} is {:?}", gamepad.name(), gamepad.power_info());
println!("Has Force Feedback Enabled ? {}", gamepad.is_ff_supported());
test_ff(&mut gilrs, gamepad);
counter += 1;
}
fn main() -> Result<()> {
color_eyre::install()?;
if counter <= 0 {
println!("No gamepads found!");
}
init_tracing();
let mut manager = GamepadManager::new()?;
manager.scan_gamepads();
// if manager.connected_gamepads == 0 {
// return Err(eyre!("No gamepads found!"));
// }
let terminal = ratatui::init();
let app = App::new(manager);
let app_result = app.run(terminal);
ratatui::restore();
app_result
}
fn test_ff(gilrs: &mut Gilrs, gamepad: Gamepad) {
let duration: u32 = 1000;
let ff_play_ticks = Ticks::from_ms(duration);
let effect = EffectBuilder::new()
.add_effect(BaseEffect {
kind: BaseEffectType::Strong { magnitude: 40_000 },
scheduling: Replay {
play_for: ff_play_ticks,
with_delay: ff_play_ticks * 2,
..Default::default()
},
envelope: Default::default(),
})
.add_effect(BaseEffect {
kind: BaseEffectType::Weak { magnitude: 40_000 },
scheduling: Replay {
after: ff_play_ticks,
play_for: ff_play_ticks,
..Default::default()
},
envelope: Default::default(),
})
.add_gamepad(&gamepad)
.finish(gilrs)
#[tracing::instrument]
fn init_tracing() {
// Create logs folder if it doesn't exist
let logs_path = std::path::Path::new("logs/");
if !logs_path.exists() {
std::fs::create_dir("logs/").expect("Could not create a logs folder at startup!");
}
let log_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open("logs/latest.log")
.unwrap();
effect.play().unwrap();
let sub = tracing_subscriber::registry().with(
tracing_subscriber::fmt::layer()
.compact()
.with_ansi(false)
.with_target(false)
.with_file(true)
.with_line_number(true)
.with_writer(log_file)
.with_filter(tracing_subscriber::filter::LevelFilter::from_level(
tracing::Level::INFO,
))
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()),
);
thread::sleep(Duration::from_millis((duration * 2) as u64));
tracing::subscriber::set_global_default(sub).unwrap();
}