Bevy+Ratutui powered Monitoring of Pico-Strike devices
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}