first commit
Some checks failed
Rust / Clippy (push) Has been cancelled
Rust / Test (nightly) (push) Has been cancelled
Rust / Test (stable) (push) Has been cancelled

This commit is contained in:
2026-01-04 16:50:19 +08:00
commit 6675986579
60 changed files with 11043 additions and 0 deletions

280
tui/src/main.rs Normal file
View File

@@ -0,0 +1,280 @@
use std::{io, time::Duration};
use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use tokio::sync::mpsc;
mod app;
mod bt;
mod maestro_client;
mod ui;
use app::App;
use maestro_client::{ClientCommand, ClientEvent};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_writer(std::io::stderr)
.init();
// 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)?;
// Create app
let mut app = App::new();
// Create channels
let (tx_event, rx_event) = mpsc::unbounded_channel();
let (tx_cmd, rx_cmd) = mpsc::unbounded_channel();
// Spawn client in a separate blocking task to isolate it completely
tokio::spawn(maestro_client::run_loop(tx_event, rx_cmd));
// Initial check
tx_cmd.send(ClientCommand::CheckConnection).ok();
let res = run_app(&mut terminal, &mut app, tx_cmd, rx_event).await;
// Restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err);
}
Ok(())
}
async fn run_app<B: ratatui::backend::Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
tx_cmd: mpsc::UnboundedSender<ClientCommand>,
mut rx_event: mpsc::UnboundedReceiver<ClientEvent>,
) -> Result<()>
where
<B as ratatui::backend::Backend>::Error: std::marker::Send + Sync + 'static,
{
let tick_rate = Duration::from_millis(50);
loop {
terminal.draw(|f| ui::draw(f, app))?;
// Poll for keyboard events with timeout - this is non-blocking
if event::poll(tick_rate)? && let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => {
app.should_quit = true;
}
KeyCode::Tab => {
app.next_tab();
}
KeyCode::Char('c') => {
tx_cmd.send(ClientCommand::CheckConnection).ok();
}
KeyCode::Down | KeyCode::Char('j') => {
if app.selected_tab == 1 {
app.next_setting();
}
}
KeyCode::Up | KeyCode::Char('k') => {
if app.selected_tab == 1 {
app.previous_setting();
}
}
KeyCode::Left => {
if app.selected_tab == 1 {
handle_numeric_change(app, &tx_cmd, -1.0);
}
}
KeyCode::Right => {
if app.selected_tab == 1 {
handle_numeric_change(app, &tx_cmd, 1.0);
}
}
KeyCode::Enter => {
if app.selected_tab == 1 {
handle_setting_change(app, &tx_cmd);
}
}
_ => {}
}
}
// Process all pending client events (non-blocking)
while let Ok(event) = rx_event.try_recv() {
process_client_event(app, &tx_cmd, event)?;
}
app.on_tick();
if app.should_quit {
return Ok(());
}
}
}
fn process_client_event(
app: &mut App,
tx_cmd: &mpsc::UnboundedSender<ClientCommand>,
event: ClientEvent,
) -> Result<()> {
match event {
ClientEvent::ConnectionState(state) => {
app.connection_state = state.clone();
if matches!(state, maestro_client::ConnectionState::Connected) {
tx_cmd.send(ClientCommand::GetSoftware)?;
tx_cmd.send(ClientCommand::GetHardware)?;
let mut fetched_keys = std::collections::HashSet::new();
for item in &app.settings {
if !fetched_keys.contains(&item.key) {
tx_cmd.send(ClientCommand::GetSetting(item.key.clone()))?;
fetched_keys.insert(item.key.clone());
}
}
tx_cmd.send(ClientCommand::GetSetting("gesture-control".to_string()))?;
}
}
ClientEvent::Software(info) => {
app.software = info;
}
ClientEvent::Hardware(info) => {
app.hardware = info;
}
ClientEvent::Runtime(info) => {
app.runtime = info.clone();
app.battery = info.battery;
}
ClientEvent::Setting(key, val) => {
if key == "gesture-control" {
app.gesture_control = val;
} else if key == "eq" {
let trimmed = val.trim_matches(|c| c == '[' || c == ']');
let parts: Vec<&str> = trimmed.split(',').map(|s| s.trim()).collect();
if parts.len() == 5 {
for (i, part) in parts.iter().enumerate() {
if part.parse::<f32>().is_ok()
&& let Some(item) = app.settings.iter_mut()
.find(|it| it.key == "eq" && it.index == Some(i))
{
item.value = part.to_string();
}
}
}
} else {
app.update_setting(key, val);
}
}
ClientEvent::Error(msg) => {
app.set_error(msg);
}
}
Ok(())
}
fn handle_numeric_change(app: &mut App, tx_cmd: &mpsc::UnboundedSender<ClientCommand>, direction: f32) {
if let Some(idx) = app.settings_state.selected() {
let item = &mut app.settings[idx];
if let Some((min, max, step)) = item.range {
let change = direction * step;
if item.key == "balance" {
// Parse current balance
let current_val = if item.value.contains("left:") {
let parts: Vec<&str> = item.value.split(',').collect();
let mut l = 100;
let mut r = 100;
for part in parts {
if let Some(v) = part.split(':').nth(1) {
let n = v.trim().trim_end_matches('%').parse::<i32>().unwrap_or(100);
if part.contains("left") { l = n; }
if part.contains("right") { r = n; }
}
}
if r == 100 {
100 - l
} else {
r - 100
}
} else {
0
};
let new_val = (current_val as f32 + change).clamp(min, max) as i32;
let l = (100 - new_val).min(100);
let r = (100 + new_val).min(100);
item.value = format!("left: {}%, right: {}%", l, r);
let _ = tx_cmd.send(ClientCommand::SetSetting(item.key.clone(), new_val.to_string()));
} else if item.key == "eq" {
let current_val = item.value.parse::<f32>().unwrap_or(0.0);
let new_val = (current_val + change).clamp(min, max);
item.value = format!("{:.2}", new_val);
}
}
}
// Split logic to avoid borrow checker issues for EQ
if let Some(idx) = app.settings_state.selected() {
let key = app.settings[idx].key.clone();
if key == "eq" {
let mut eq_values = [0.0f32; 5];
for it in &app.settings {
if it.key == "eq"
&& let Some(i) = it.index
&& i < 5 {
eq_values[i] = it.value.parse::<f32>().unwrap_or(0.0);
}
}
let args = eq_values.iter().map(|v| format!("{:.2}", v)).collect::<Vec<_>>().join(" ");
let _ = tx_cmd.send(ClientCommand::SetSetting("eq".to_string(), args));
}
}
}
fn handle_setting_change(app: &App, tx_cmd: &mpsc::UnboundedSender<ClientCommand>) {
if let Some(idx) = app.settings_state.selected() {
let item = &app.settings[idx];
if item.options.is_empty() {
return;
}
let current_val = item.value.to_lowercase();
let current_opt_idx = item.options.iter().position(|o| o.to_lowercase() == current_val);
let next_val = if let Some(i) = current_opt_idx {
item.options[(i + 1) % item.options.len()].clone()
} else if !item.options.is_empty() {
item.options[0].clone()
} else {
return;
};
let _ = tx_cmd.send(ClientCommand::SetSetting(item.key.clone(), next_val));
}
}