/ apps / microtop / src / app / reducer.rs
reducer.rs
  1  use super::{App, AppAction, AppEffect, COMMAND_PALETTE_ITEMS, FocusArea};
  2  
  3  impl App {
  4      pub fn close_command_palette(&mut self) {
  5          self.command_palette_open = false;
  6          self.command_palette_query.clear();
  7          self.command_palette_selected_index = 0;
  8      }
  9  
 10      pub fn open_command_palette(&mut self) {
 11          self.command_palette_open = true;
 12          self.command_palette_query.clear();
 13          self.command_palette_selected_index = 0;
 14      }
 15  
 16      pub fn open_api_base_url_editor(&mut self) {
 17          self.api_base_url_editor_open = true;
 18          self.api_base_url_editor_buffer = self.api_base_url.clone();
 19      }
 20  
 21      pub fn close_api_base_url_editor(&mut self) {
 22          self.api_base_url_editor_open = false;
 23          self.api_base_url_editor_buffer.clear();
 24      }
 25  
 26      fn apply_api_base_url_editor(&mut self) {
 27          let trimmed_value = self.api_base_url_editor_buffer.trim();
 28          if trimmed_value.is_empty() {
 29              self.close_api_base_url_editor();
 30              return;
 31          }
 32  
 33          self.api_base_url =
 34              if trimmed_value.starts_with("http://") || trimmed_value.starts_with("https://") {
 35                  trimmed_value.trim_end_matches('/').to_owned()
 36              } else {
 37                  format!("http://{}", trimmed_value.trim_end_matches('/'))
 38              };
 39  
 40          self.close_api_base_url_editor();
 41      }
 42  
 43      pub fn filtered_command_item_indices(&self) -> Vec<usize> {
 44          let query = self.command_palette_query.trim().to_lowercase();
 45          COMMAND_PALETTE_ITEMS
 46              .iter()
 47              .enumerate()
 48              .filter_map(|(index, command_palette_item)| {
 49                  if query.is_empty() {
 50                      return Some(index);
 51                  }
 52  
 53                  let haystack = format!(
 54                      "{} {} {}",
 55                      command_palette_item.label,
 56                      command_palette_item.keywords,
 57                      command_palette_item.shortcut
 58                  )
 59                  .to_lowercase();
 60  
 61                  if haystack.contains(&query) {
 62                      Some(index)
 63                  } else {
 64                      None
 65                  }
 66              })
 67              .collect()
 68      }
 69  
 70      pub fn command_palette_selected_index(&self) -> usize {
 71          self.command_palette_selected_index
 72      }
 73  
 74      fn normalize_command_palette_selection(&mut self) {
 75          let filtered_length = self.filtered_command_item_indices().len();
 76          if filtered_length == 0 {
 77              self.command_palette_selected_index = 0;
 78              return;
 79          }
 80  
 81          if self.command_palette_selected_index >= filtered_length {
 82              self.command_palette_selected_index = filtered_length - 1;
 83          }
 84      }
 85  
 86      fn move_command_palette_selection_previous(&mut self) {
 87          let filtered_length = self.filtered_command_item_indices().len();
 88          if filtered_length == 0 {
 89              self.command_palette_selected_index = 0;
 90              return;
 91          }
 92  
 93          self.command_palette_selected_index =
 94              Self::wrapped_previous_index(self.command_palette_selected_index, filtered_length);
 95      }
 96  
 97      fn move_command_palette_selection_next(&mut self) {
 98          let filtered_length = self.filtered_command_item_indices().len();
 99          if filtered_length == 0 {
100              self.command_palette_selected_index = 0;
101              return;
102          }
103  
104          self.command_palette_selected_index =
105              Self::wrapped_next_index(self.command_palette_selected_index, filtered_length);
106      }
107  
108      fn append_command_palette_query_char(&mut self, character: char) {
109          self.command_palette_query.push(character);
110          self.normalize_command_palette_selection();
111      }
112  
113      fn backspace_command_palette_query(&mut self) {
114          self.command_palette_query.pop();
115          self.normalize_command_palette_selection();
116      }
117  
118      fn execute_command_palette_selection(&mut self) -> Option<AppAction> {
119          let filtered_indices = self.filtered_command_item_indices();
120          let Some(command_palette_item_index) = filtered_indices
121              .get(self.command_palette_selected_index)
122              .copied()
123          else {
124              return None;
125          };
126  
127          let selected_action = COMMAND_PALETTE_ITEMS[command_palette_item_index].action;
128          self.close_command_palette();
129          Some(selected_action)
130      }
131  
132      pub fn reduce_action(&mut self, action: AppAction) -> (Option<AppAction>, Option<AppEffect>) {
133          match action {
134              AppAction::Quit => self.exit = true,
135              AppAction::NextPane => self.focus_area = self.focus_area.next(),
136              AppAction::PreviousPane => self.focus_area = self.focus_area.previous(),
137              AppAction::FocusMeasurementsPane => self.focus_area = FocusArea::Measurements,
138              AppAction::FocusNetworkPane => self.focus_area = FocusArea::Network,
139              AppAction::FocusFileSystemPane => self.focus_area = FocusArea::FileSystem,
140              AppAction::MoveSelectionUp => self.move_selection_previous(),
141              AppAction::MoveSelectionDown => self.move_selection_next(),
142              AppAction::SelectPreviousMeasurementTab => {
143                  self.measurement_tab = self.measurement_tab.previous();
144              }
145              AppAction::SelectNextMeasurementTab => {
146                  self.measurement_tab = self.measurement_tab.next();
147              }
148              AppAction::RefreshFileSystem => return (None, Some(AppEffect::RefreshFileSystem)),
149              AppAction::ScanNetwork => return (None, Some(AppEffect::ScanNetwork)),
150              AppAction::OpenCommandPalette => self.open_command_palette(),
151              AppAction::CloseCommandPalette => self.close_command_palette(),
152              AppAction::CommandPaletteInputChar(character) => {
153                  self.append_command_palette_query_char(character)
154              }
155              AppAction::CommandPaletteBackspace => self.backspace_command_palette_query(),
156              AppAction::CommandPaletteSelectUp => self.move_command_palette_selection_previous(),
157              AppAction::CommandPaletteSelectDown => self.move_command_palette_selection_next(),
158              AppAction::CommandPaletteExecute => {
159                  return (self.execute_command_palette_selection(), None);
160              }
161              AppAction::OpenApiBaseUrlEditor => self.open_api_base_url_editor(),
162              AppAction::CloseApiBaseUrlEditor => self.close_api_base_url_editor(),
163              AppAction::ApiBaseUrlEditorInputChar(character) => {
164                  self.api_base_url_editor_buffer.push(character)
165              }
166              AppAction::ApiBaseUrlEditorBackspace => {
167                  self.api_base_url_editor_buffer.pop();
168              }
169              AppAction::ApplyApiBaseUrlEditor => self.apply_api_base_url_editor(),
170              AppAction::MouseHover { column, row } => {
171                  if !self.command_palette_open
172                      && !self.api_base_url_editor_open
173                      && let Some(focus_area) = self.dashboard_areas.focus_area_at(column, row)
174                  {
175                      self.focus_area = focus_area;
176                  }
177              }
178              AppAction::Noop => {}
179          }
180  
181          (None, None)
182      }
183  
184      fn wrapped_previous_index(current_index: usize, length: usize) -> usize {
185          if current_index == 0 {
186              length - 1
187          } else {
188              current_index - 1
189          }
190      }
191  
192      fn wrapped_next_index(current_index: usize, length: usize) -> usize {
193          if current_index + 1 >= length {
194              0
195          } else {
196              current_index + 1
197          }
198      }
199  
200      fn move_selection_previous(&mut self) {
201          match self.focus_area {
202              FocusArea::Measurements => {}
203              FocusArea::Network => {
204                  if self.wireless_networks.is_empty() {
205                      self.network_table_state.select(None);
206                      return;
207                  }
208                  let current_index = self.network_table_state.selected().unwrap_or(0);
209                  let previous_index =
210                      Self::wrapped_previous_index(current_index, self.wireless_networks.len());
211                  self.network_table_state.select(Some(previous_index));
212              }
213              FocusArea::FileSystem => {
214                  #[cfg(not(target_arch = "wasm32"))]
215                  {
216                      self.file_system_tree_state.key_up();
217                  }
218  
219                  #[cfg(target_arch = "wasm32")]
220                  {
221                      let total_rows = self.file_system_render_row_count();
222                      if total_rows == 0 {
223                          self.file_system_list_state.select(None);
224                          return;
225                      }
226                      let current_index = self.file_system_list_state.selected().unwrap_or(0);
227                      let previous_index = Self::wrapped_previous_index(current_index, total_rows);
228                      self.file_system_list_state.select(Some(previous_index));
229                  }
230              }
231          }
232      }
233  
234      fn move_selection_next(&mut self) {
235          match self.focus_area {
236              FocusArea::Measurements => {}
237              FocusArea::Network => {
238                  if self.wireless_networks.is_empty() {
239                      self.network_table_state.select(None);
240                      return;
241                  }
242                  let current_index = self.network_table_state.selected().unwrap_or(0);
243                  let next_index =
244                      Self::wrapped_next_index(current_index, self.wireless_networks.len());
245                  self.network_table_state.select(Some(next_index));
246              }
247              FocusArea::FileSystem => {
248                  #[cfg(not(target_arch = "wasm32"))]
249                  {
250                      self.file_system_tree_state.key_down();
251                  }
252  
253                  #[cfg(target_arch = "wasm32")]
254                  {
255                      let total_rows = self.file_system_render_row_count();
256                      if total_rows == 0 {
257                          self.file_system_list_state.select(None);
258                          return;
259                      }
260                      let current_index = self.file_system_list_state.selected().unwrap_or(0);
261                      let next_index = Self::wrapped_next_index(current_index, total_rows);
262                      self.file_system_list_state.select(Some(next_index));
263                  }
264              }
265          }
266      }
267  }