Started working on a TUI tester, has keyboard and gamepad input handlers
parent
0aa7f846b1
commit
4b87e6c013
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
|
@ -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
|
||||
codegen-units = 1
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
100
src/main.rs
100
src/main.rs
|
@ -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();
|
||||
}
|
Loading…
Reference in New Issue