Bevy+Ratutui powered Monitoring of Pico-Strike devices
at main 167 lines 5.5 kB view raw
1use bevy::{ 2 ecs::{ 3 entity::Entity, 4 error::Result, 5 message::MessageReader, 6 name::Name, 7 query::With, 8 resource::Resource, 9 system::{Commands, Query, Res, ResMut}, 10 world::World, 11 }, 12 state::state::NextState, 13 time::{Real, Time, Timer}, 14}; 15use bevy_ratatui::RatatuiContext; 16use ratatui::{ 17 layout::{Constraint, HorizontalAlignment, Layout}, 18 style::{Color, Style, Stylize}, 19 widgets::{Block, List, ListDirection, ListItem, ListState, Padding, Paragraph}, 20}; 21 22use crate::{ 23 device::{ConnectedDevice, ConnectionState, Device, DeviceSocket}, 24 messages::StrikeMessage, 25 net::MdnsSignaler, 26 state::AppState, 27}; 28 29#[derive(Debug, Default, Resource)] 30pub struct DeviceListState(ListState); 31 32#[derive(Debug, Default, Resource)] 33pub struct SearchingDevices { 34 pub searching: Option<Timer>, 35} 36 37pub fn setup_home_view(mut commands: Commands) { 38 commands.init_resource::<SearchingDevices>(); 39 commands.init_resource::<DeviceListState>(); 40} 41 42pub fn cleanup_home_view(mut commands: Commands) { 43 commands.remove_resource::<DeviceListState>(); 44 commands.remove_resource::<SearchingDevices>(); 45} 46 47pub fn home_message_handler( 48 signal: Res<MdnsSignaler>, 49 mut list_state: ResMut<DeviceListState>, 50 mut is_searching: ResMut<SearchingDevices>, 51 mut strike_reader: MessageReader<StrikeMessage>, 52 mut commands: Commands, 53) { 54 for message in strike_reader.read() { 55 match message { 56 StrikeMessage::ToggleSearch => { 57 if is_searching.searching.is_some() { 58 is_searching.searching = None; 59 } else { 60 is_searching.searching = 61 Some(Timer::from_seconds(1.0, bevy::time::TimerMode::Once)); 62 } 63 let _ = signal.0.try_send(()); 64 } 65 StrikeMessage::NextDevice if is_searching.searching.is_none() => { 66 list_state.0.select_next(); 67 } 68 StrikeMessage::PrevDevice if is_searching.searching.is_none() => { 69 list_state.0.select_previous(); 70 } 71 StrikeMessage::MonitorDevice if is_searching.searching.is_none() => { 72 let offset = list_state.0.offset(); 73 74 commands.queue(move |world: &mut World| -> Result { 75 let device = world 76 .query_filtered::<Entity, With<Device>>() 77 .iter(world) 78 .nth(offset) 79 .unwrap(); 80 world.insert_resource(ConnectedDevice { 81 device, 82 connection_state: ConnectionState::Disconnected, 83 }); 84 let mut next_state = world.resource_mut::<NextState<AppState>>(); 85 next_state.set(AppState::Monitoring); 86 87 Ok(()) 88 }); 89 } 90 _ => {} 91 } 92 } 93} 94 95pub fn home_view( 96 mut context: ResMut<RatatuiContext>, 97 is_searching: Res<SearchingDevices>, 98 mut list_state: ResMut<DeviceListState>, 99 q_devices: Query<(&Name, &DeviceSocket), With<Device>>, 100) -> Result { 101 context.draw(|frame| { 102 let [top, mid, bottom] = Layout::vertical([ 103 Constraint::Length(3), 104 Constraint::Fill(1), 105 Constraint::Length(3), 106 ]) 107 .areas(frame.area()); 108 109 let searching = if is_searching.searching.is_some() { 110 "Searching..." 111 } else { 112 "Select a Device" 113 }; 114 let paragraph = Paragraph::new(searching).block( 115 Block::bordered() 116 .padding(Padding::horizontal(2)) 117 .title("Striker") 118 .title_alignment(HorizontalAlignment::Center) 119 .border_style(Color::LightBlue), 120 ).white(); 121 122 let items = q_devices.iter().map(|(name, addr)| { 123 ListItem::new(format!( 124 "{}, {}:{}, {:?}", 125 name, addr.address, addr.port, addr.ip 126 )) 127 }); 128 129 let help_text = "Keys: 'q'/ESC Quit, 's' Toggle Search, UP/DOWN Choose Device(s), ENTER/SPACE Connect to Device"; 130 131 let help = Paragraph::new(help_text) 132 .block( 133 Block::bordered() 134 .padding(Padding::horizontal(2)) 135 .border_style(Color::LightBlue), 136 ).white(); 137 138 let list = List::new(items) 139 .highlight_symbol(">> ") 140 .highlight_spacing(ratatui::widgets::HighlightSpacing::Always) 141 .highlight_style(Style::new().on_dark_gray().black()) 142 .direction(ListDirection::TopToBottom) 143 .block( 144 Block::bordered() 145 .title("Devices") 146 .title_alignment(HorizontalAlignment::Center) 147 .padding(Padding::new(2, 2, 1, 1)) 148 .border_style(Color::LightBlue), 149 ) 150 .white(); 151 152 frame.render_widget(paragraph, top); 153 frame.render_stateful_widget(list, mid, &mut list_state.0); 154 frame.render_widget(help, bottom); 155 })?; 156 157 Ok(()) 158} 159 160pub fn search_timer(mut is_searching: ResMut<SearchingDevices>, time: Res<Time<Real>>) { 161 if let Some(timer) = &mut is_searching.searching { 162 timer.tick(time.delta()); 163 if timer.is_finished() { 164 is_searching.searching = None; 165 } 166 } 167}