import upstream maccel baseline
Tests / test_core_function (push) Failing after 12s

This commit is contained in:
2026-03-24 12:10:31 +00:00
parent 6e948d7b39
commit 5f1254d11a
108 changed files with 18930 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
use gsf_core::AccelMode;
#[derive(Debug, PartialEq)]
pub enum InputAction {
Enter,
Focus,
Reset,
}
#[derive(Debug, PartialEq)]
pub enum Action {
Tick,
Input(InputAction),
SelectNextInput,
SelectPreviousInput,
SetMode(AccelMode),
ScrollDown,
ScrollUp,
ScrollPageDown,
ScrollPageUp,
}
pub type Actions = Vec<Action>;
+285
View File
@@ -0,0 +1,285 @@
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use gsf_core::ALL_COMMON_PARAMS;
use gsf_core::ALL_LINEAR_PARAMS;
use gsf_core::ALL_MODES;
use gsf_core::ALL_NATURAL_PARAMS;
use gsf_core::ALL_NOACCEL_PARAMS;
use gsf_core::ALL_SYNCHRONOUS_PARAMS;
use gsf_core::Param;
use gsf_core::get_param_value_from_ctx;
use gsf_core::persist::ParamStore;
use gsf_core::persist::SysFsStore;
use gsf_core::{ALL_PARAMS, AccelMode, ContextRef, TuiContext};
use ratatui::Terminal;
use ratatui::backend::Backend;
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEventKind};
use std::{io, time::Instant};
use std::{panic, time::Duration};
use tracing::debug;
use crate::action::{Action, Actions};
use crate::component::TuiComponent;
use crate::event::{Event, EventHandler};
use crate::graph::SensitivityGraph;
use crate::param_input::ParameterInput;
use crate::screen::Screen;
use crate::utils::CyclingIdx;
pub struct App {
context: ContextRef<SysFsStore>,
screens: Vec<Screen<SysFsStore>>,
screen_idx: CyclingIdx,
pub(crate) is_running: bool,
last_tick_at: Instant,
}
pub fn collect_inputs_for_params(
params: &[Param],
context: ContextRef<SysFsStore>,
) -> Vec<ParameterInput<SysFsStore>> {
ALL_COMMON_PARAMS
.iter()
.chain(params)
.filter_map(|&p| context.get().parameter(p).copied())
.map(|param| ParameterInput::new(&param, context.clone()))
.collect()
}
impl App {
pub fn new() -> Self {
let context = ContextRef::new(
TuiContext::init(SysFsStore, ALL_PARAMS).expect("Failed to initialize the Tui Context"),
);
Self {
screens: vec![
Screen::new(
AccelMode::Linear,
collect_inputs_for_params(ALL_LINEAR_PARAMS, context.clone()),
Box::new(
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
// Appropriate dynamic bounds for the Linear sens graph
let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult))
* f64::from(get_param_value_from_ctx!(ctx, OutputCap)).max(1.0)
* 2.0;
[0.0, upper_bound]
}),
),
),
Screen::new(
AccelMode::Natural,
collect_inputs_for_params(ALL_NATURAL_PARAMS, context.clone()),
Box::new(
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
// Appropriate dynamic bounds for the Natural sens graph
let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult))
* f64::from(get_param_value_from_ctx!(ctx, Limit)).max(1.0)
* 2.0;
[0.0, upper_bound]
}),
),
),
Screen::new(
AccelMode::Synchronous,
collect_inputs_for_params(ALL_SYNCHRONOUS_PARAMS, context.clone()),
Box::new(
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
// Appropriate dynamic bounds for the Synchronous sens graph
let upper_bound = f64::from(get_param_value_from_ctx!(ctx, SensMult))
* f64::from(get_param_value_from_ctx!(ctx, Motivity)).max(1.0)
* 2.0;
[0.0, upper_bound]
}),
),
),
Screen::new(
AccelMode::NoAccel,
collect_inputs_for_params(ALL_NOACCEL_PARAMS, context.clone()),
Box::new(
SensitivityGraph::new(context.clone()).on_y_axix_bounds_update(|ctx| {
// Appropriate dynamic bounds for the NoAccel sens graph
let upper_bound =
f64::from(get_param_value_from_ctx!(ctx, SensMult)) * 2.0; // No other factor, sens is 1.0
[0.0, upper_bound.max(1.0)] // Ensure at least a small visible range
}),
),
),
],
screen_idx: CyclingIdx::new_starting_at(
ALL_MODES.len(),
context.clone().get().current_mode.ordinal() as usize,
),
context,
is_running: true,
last_tick_at: Instant::now(),
}
}
pub(crate) fn tick(&mut self) -> bool {
#[cfg(not(debug_assertions))]
let tick_rate = 16;
#[cfg(debug_assertions)]
let tick_rate = 100;
let do_tick = self.last_tick_at.elapsed() >= Duration::from_millis(tick_rate);
do_tick.then(|| {
self.last_tick_at = Instant::now();
});
do_tick
}
fn current_screen_mut(&mut self) -> &mut Screen<SysFsStore> {
let screen_idx = self.screen_idx.current();
self.screens.get_mut(screen_idx).unwrap_or_else(|| {
panic!(
"Failed to get a Screen for mode id {}: {:?}",
screen_idx, ALL_MODES[screen_idx]
);
})
}
fn current_screen(&self) -> &Screen<SysFsStore> {
let screen_idx = self.screen_idx.current();
self.screens.get(screen_idx).unwrap_or_else(|| {
panic!(
"Failed to get a Screen for mode id {}: {:?}",
screen_idx, ALL_MODES[screen_idx]
);
})
}
fn can_switch_screens(&self) -> bool {
self.screens.len() > 1 && !self.current_screen().is_in_editing_mode()
}
}
impl App {
pub(crate) fn handle_event(&mut self, event: &Event, actions: &mut Actions) {
debug!("received event: {:?}", event);
if let Event::Key(crossterm::event::KeyEvent {
kind: KeyEventKind::Press,
code,
..
}) = event
{
match code {
KeyCode::Char('q') => {
self.is_running = false;
return;
}
KeyCode::Right => {
if self.can_switch_screens() {
self.screen_idx.forward();
actions.push(Action::SetMode(
self.screens[self.screen_idx.current()].accel_mode,
));
}
}
KeyCode::Left => {
if self.can_switch_screens() {
self.screen_idx.back();
actions.push(Action::SetMode(
self.screens[self.screen_idx.current()].accel_mode,
));
}
}
_ => {}
}
}
self.current_screen_mut().handle_event(event, actions);
}
pub(crate) fn update(&mut self, actions: &mut Vec<Action>) {
debug!("performing actions: {actions:?}");
for action in actions.drain(..) {
if let Action::SetMode(accel_mode) = action {
self.context.get_mut().current_mode = accel_mode;
SysFsStore
.set_current_accel_mode(accel_mode)
.expect("Failed to set accel mode in TUI");
self.context.get_mut().reset_current_parameters();
}
self.current_screen_mut().update(&action);
}
}
pub(crate) fn draw(&self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) {
self.current_screen().draw(frame, area);
}
}
/// Representation of a terminal user interface.
///
/// It is responsible for setting up the terminal,
/// initializing the interface and handling the draw events.
#[derive(Debug)]
pub struct Tui<B: Backend> {
/// Interface to the Terminal.
terminal: Terminal<B>,
/// Terminal event handler.
pub events: EventHandler,
}
impl<B: Backend> Tui<B> {
/// Constructs a new instance of [`Tui`].
pub fn new(terminal: Terminal<B>, events: EventHandler) -> Self {
Self { terminal, events }
}
/// Initializes the terminal interface.
///
/// It enables the raw mode and sets terminal properties.
pub fn init(&mut self) -> anyhow::Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
// Define a custom panic hook to reset the terminal properties.
// This way, you won't have your terminal messed up if an unexpected error happens.
let panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic| {
Self::reset().expect("failed to reset the terminal");
panic_hook(panic);
}));
self.terminal.hide_cursor()?;
self.terminal.clear()?;
Ok(())
}
/// [`Draw`] the terminal interface by [`rendering`] the widgets.
///
/// [`Draw`]: ratatui::Terminal::draw
/// [`rendering`]: crate::ui::render
pub fn draw(&mut self, app: &mut App) -> anyhow::Result<()> {
self.terminal.draw(|frame| app.draw(frame, frame.area()))?;
Ok(())
}
/// Resets the terminal interface.
///
/// This function is also used for the panic hook to revert
/// the terminal properties if unexpected errors occur.
fn reset() -> anyhow::Result<()> {
crossterm::terminal::disable_raw_mode()?;
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
Ok(())
}
/// Exits the terminal interface.
///
/// It disables the raw mode and reverts back the terminal properties.
pub fn exit(&mut self) -> anyhow::Result<()> {
Self::reset()?;
self.terminal.show_cursor()?;
Ok(())
}
}
+38
View File
@@ -0,0 +1,38 @@
use std::fmt::Debug;
use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{Frame, layout::Rect};
use super::{
action::{Action, Actions},
event::Event,
};
pub trait TuiComponent {
fn handle_event(&mut self, event: &Event, actions: &mut Actions) {
match event {
Event::Key(key_event) => self.handle_key_event(key_event, actions),
Event::Mouse(mouse_event) => self.handle_mouse_event(mouse_event, actions),
}
}
fn handle_key_event(&mut self, event: &KeyEvent, actions: &mut Actions);
fn handle_mouse_event(&mut self, event: &MouseEvent, actions: &mut Actions);
/// Stuff to do on any action derived from an event
fn update(&mut self, action: &Action);
fn draw(&self, frame: &mut Frame, area: Rect);
}
#[derive(Debug)]
pub struct NoopComponent;
impl TuiComponent for NoopComponent {
fn handle_key_event(&mut self, _event: &KeyEvent, _actions: &mut Actions) {}
fn handle_mouse_event(&mut self, _event: &MouseEvent, _actions: &mut Actions) {}
fn update(&mut self, _action: &Action) {}
fn draw(&self, _frame: &mut Frame, _area: Rect) {}
}
+52
View File
@@ -0,0 +1,52 @@
use std::sync::mpsc::TryRecvError;
use crossterm::event::Event as CrosstermEvent;
use crossterm::event::MouseEventKind;
#[derive(Debug)]
pub enum Event {
Key(crossterm::event::KeyEvent),
Mouse(crossterm::event::MouseEvent),
}
#[derive(Debug)]
pub struct EventHandler {
rx: std::sync::mpsc::Receiver<Event>,
}
impl EventHandler {
pub fn new() -> Self {
let tick_rate = std::time::Duration::from_millis(33);
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || -> anyhow::Result<()> {
loop {
if crossterm::event::poll(tick_rate)? {
match crossterm::event::read()? {
CrosstermEvent::Key(e) => tx.send(Event::Key(e)),
CrosstermEvent::Mouse(e) => {
if e.kind == MouseEventKind::Moved {
// These are annoying because they get buffered
// while you move your mouse a bunch and delay
// other events, e.g 'q' quitting could much longer than it should.
continue;
}
tx.send(Event::Mouse(e))
}
CrosstermEvent::Resize(_col, _row) => continue,
event => unimplemented!("event {event:?}"),
}?
}
}
});
EventHandler { rx }
}
pub fn next(&self) -> anyhow::Result<Option<Event>> {
Ok(match self.rx.try_recv() {
Ok(e) => Some(e),
Err(TryRecvError::Empty) => None,
Err(err) => return Err(err.into()),
})
}
}
+51
View File
@@ -0,0 +1,51 @@
use gsf_core::inputspeed;
use event::EventHandler;
use ratatui::{Terminal, prelude::CrosstermBackend};
mod event;
mod action;
mod app;
mod component;
mod graph;
mod param_input;
mod screen;
mod utils;
mod widgets;
pub fn run_tui() -> anyhow::Result<()> {
let mut app = app::App::new();
let backend = CrosstermBackend::new(std::io::stdout());
let terminal = Terminal::new(backend)?;
let events = EventHandler::new();
let mut tui = app::Tui::new(terminal, events);
tui.init()?;
let input_speed_thread_handle = inputspeed::setup_input_speed_reader();
let mut actions = vec![];
while app.is_running {
if let Some(event) = tui.events.next()? {
app.handle_event(&event, &mut actions);
app.update(&mut actions);
}
if app.tick() {
app.update(&mut vec![action::Action::Tick]);
tui.draw(&mut app)?;
}
}
tui.exit()?;
if input_speed_thread_handle.is_finished() {
input_speed_thread_handle
.join()
.expect("couldn't join on (finished) inputspeed reader thread")?;
}
Ok(())
}
+219
View File
@@ -0,0 +1,219 @@
use std::fmt::Debug;
use anyhow::Context;
use gsf_core::persist::ParamStore;
use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::Rect;
use ratatui::{prelude::*, widgets::*};
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;
use crate::action::{Action, Actions, InputAction};
use crate::component::TuiComponent;
use gsf_core::{ContextRef, Param, Parameter};
#[derive(Debug, PartialEq)]
pub enum InputMode {
Normal,
Editing,
}
#[derive(Debug)]
pub struct ParameterInput<PS: ParamStore> {
context: ContextRef<PS>,
param_tag: Param,
input: Input,
pub input_mode: InputMode,
error: Option<String>,
pub is_selected: bool,
}
impl<PS: ParamStore> ParameterInput<PS> {
pub fn new(param: &Parameter, context: ContextRef<PS>) -> Self {
Self {
context,
param_tag: param.tag,
input_mode: InputMode::Normal,
input: format!("{}", param.value).into(),
error: None,
is_selected: false,
}
}
fn this_param(&self) -> Parameter {
*self
.context
.get()
.parameter(self.param_tag)
.expect("Failed to get param from context")
}
pub fn param(&self) -> Param {
self.param_tag
}
pub fn value(&self) -> &str {
self.input.value()
}
fn update_value(&mut self) {
let value = self
.value()
.parse()
.context("should be a number")
.and_then(|value| {
self.context
.get_mut()
.update_param_value(self.param_tag, value)
});
match value {
Ok(_) => {
self.error = None;
}
Err(err) => {
self.reset();
self.error = Some(format!("{err:#}"));
}
}
self.input_mode = InputMode::Normal;
}
fn reset(&mut self) {
self.error = None;
self.input = format!("{}", self.this_param().value).into();
self.input_mode = InputMode::Normal;
}
}
impl<PS: ParamStore + Debug> TuiComponent for ParameterInput<PS> {
fn handle_key_event(&mut self, key: &KeyEvent, actions: &mut Actions) {
if !self.is_selected {
return;
}
let action = match self.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('i') | KeyCode::Enter => InputAction::Focus,
KeyCode::BackTab | KeyCode::Up => {
actions.push(Action::SelectPreviousInput);
return;
}
KeyCode::Tab | KeyCode::Down => {
actions.push(Action::SelectNextInput);
return;
}
KeyCode::PageDown => {
actions.push(Action::ScrollPageDown);
return;
}
KeyCode::PageUp => {
actions.push(Action::ScrollPageUp);
return;
}
_ => return,
},
InputMode::Editing => match key.code {
KeyCode::Enter => InputAction::Enter,
KeyCode::Esc => InputAction::Reset,
_ => {
let _ = self
.input
.handle_event(&::crossterm::event::Event::Key(*key));
return;
}
},
};
actions.push(Action::Input(action));
}
fn handle_mouse_event(
&mut self,
_event: &::crossterm::event::MouseEvent,
_actions: &mut Actions,
) {
}
fn update(&mut self, action: &Action) {
if let Action::SetMode(_) = action {
self.reset();
}
if !self.is_selected {
return;
}
if let Action::Input(action) = action {
match action {
InputAction::Enter => {
self.update_value();
}
InputAction::Reset => self.reset(),
InputAction::Focus => self.input_mode = InputMode::Editing,
};
}
}
fn draw(&self, frame: &mut ratatui::Frame, area: Rect) {
let input_group_layout = Layout::new(
Direction::Vertical,
[Constraint::Min(0), Constraint::Length(2)],
)
.split(area);
let input_layout = input_group_layout[0];
let input_width = area.width.max(3) - 3; // keep 2 for borders and 1 for cursor
let input_scroll_position = self.input.visual_scroll(input_width as usize);
let mut input = Paragraph::new(self.input.value())
.style(match self.input_mode {
InputMode::Normal => match self.is_selected {
true => Style::default().light_blue(),
false => Style::default(),
},
InputMode::Editing => Style::default().bold().light_blue(),
})
.scroll((0, input_scroll_position as u16))
.block(
Block::default()
.borders(Borders::ALL)
.title(self.this_param().tag.display_name()),
);
match self.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
frame.set_cursor_position((
// Put cursor past the end of the input text
input_layout.x
+ ((self.input.visual_cursor()).max(input_scroll_position)
- input_scroll_position) as u16
+ 1,
// Move one line down, from the border to the input line
input_layout.y + 1,
))
}
}
let helper_text_layout = input_group_layout[1];
if let Some(error) = &self.error {
let helper_text = Paragraph::new(error.as_str())
.red()
.wrap(ratatui::widgets::Wrap { trim: true });
frame.render_widget(helper_text, helper_text_layout);
input = input.red();
}
frame.render_widget(input, input_layout);
}
}
+98
View File
@@ -0,0 +1,98 @@
#[derive(Debug)]
pub struct CyclingIdx {
idx: usize,
n: usize,
}
impl CyclingIdx {
pub fn new(n: usize) -> Self {
Self::new_starting_at(n, 0)
}
pub fn new_starting_at(n: usize, idx: usize) -> Self {
Self { idx, n }
}
pub fn current(&self) -> usize {
debug_assert!(self.idx < self.n);
self.idx
}
pub fn forward(&mut self) {
self.idx += 1;
if self.idx >= self.n {
self.idx = 0
}
}
pub fn back(&mut self) {
if self.idx == 0 {
self.idx = self.n
}
self.idx -= 1
}
}
#[cfg(test)]
pub(crate) mod test_utils {
use gsf_core::{ContextRef, Parameter, fixedptc::Fpt};
use mocks::MockStore;
pub fn new_context() -> (ContextRef<MockStore>, Vec<Parameter>) {
let params = [
(gsf_core::Param::SensMult, 1.0),
(gsf_core::Param::Accel, 1.0),
];
let params = params.map(|(p, v)| (p, Fpt::from(v)));
let context = ContextRef::new(
gsf_core::TuiContext::init(
MockStore {
list: params.to_vec(),
},
&params.map(|(p, _)| p),
)
.unwrap(),
);
(context, params.map(|(p, v)| Parameter::new(p, v)).to_vec())
}
mod mocks {
use anyhow::Context;
use gsf_core::{AccelMode, Param, fixedptc::Fpt, persist::ParamStore};
#[derive(Debug)]
pub struct MockStore {
pub list: Vec<(Param, Fpt)>,
}
impl ParamStore for MockStore {
fn set(&mut self, param: Param, value: f64) -> anyhow::Result<()> {
if !self.list.iter().any(|(p, _)| p == &param) {
self.list.push((param, value.into()));
}
Ok(())
}
fn get(&self, param: Param) -> anyhow::Result<Fpt> {
self.list
.iter()
.find(|(p, _)| p == &param)
.map(|(_, v)| v)
.copied()
.context("failed to get param")
}
fn set_current_accel_mode(
&mut self,
_mode: gsf_core::AccelMode,
) -> anyhow::Result<()> {
unimplemented!()
}
fn get_current_accel_mode(&self) -> anyhow::Result<gsf_core::AccelMode> {
Ok(AccelMode::Linear)
}
}
}
}
+1
View File
@@ -0,0 +1 @@
pub mod scrollbar;
+70
View File
@@ -0,0 +1,70 @@
use ratatui::{prelude::*, widgets::Widget};
pub struct CustomScrollbar {
total_items: usize,
visible_items: usize,
scroll_position: usize,
}
impl CustomScrollbar {
pub fn new(total: usize, visible: usize, position: usize) -> Self {
Self {
total_items: total,
visible_items: visible,
scroll_position: position,
}
}
pub fn with_position(self, position: usize) -> Self {
Self {
scroll_position: position,
..self
}
}
}
impl Widget for CustomScrollbar {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.height < 3 || self.total_items == 0 {
return;
}
let track_height = area.height.saturating_sub(2);
let track_top = area.y + 1;
let track_bottom = track_top + track_height.saturating_sub(1);
let x = area.x;
let max_scroll = self.total_items.saturating_sub(self.visible_items);
let scroll_ratio = if max_scroll > 0 {
(self.scroll_position as f64) / (max_scroll as f64)
} else {
0.0
};
let thumb_ratio = (self.visible_items as f64) / (self.total_items as f64);
let thumb_height = ((track_height as f64) * thumb_ratio).max(1.0).ceil() as u16;
let thumb_height = thumb_height.min(track_height);
let thumb_start = if self.scroll_position >= max_scroll {
track_bottom.saturating_sub(thumb_height - 1)
} else {
let available_space = track_height.saturating_sub(thumb_height);
let thumb_offset = (scroll_ratio * (available_space as f64)).round() as u16;
track_top + thumb_offset
};
buf.set_string(x, track_top.saturating_sub(1), "", Style::default());
buf.set_string(x, track_bottom + 1, "", Style::default());
for row in track_top..=track_bottom {
let is_thumb = row >= thumb_start && row < thumb_start + thumb_height;
let symbol = if is_thumb { "" } else { "" };
let style = if is_thumb {
Style::default().fg(Color::White)
} else {
Style::default().fg(Color::Gray)
};
buf.set_string(x, row, symbol, style);
}
}
}