1use std::path::Path;
5
6use ratatui::widgets::ListState;
7
8use super::{
9 BulkTagAction, BulkTagApplyResult, BulkTagEditorState, BulkTagRow, HostListItem,
10 ProxyJumpCandidate, Screen,
11};
12use crate::app::App;
13use crate::ssh_config::model::{HostEntry, PatternEntry};
14use crate::ssh_keys;
15
16impl App {
17 pub fn set_screen(&mut self, screen: Screen) {
21 if self.screen != screen {
22 log::debug!(
23 "[purple] screen: {} → {}",
24 self.screen.variant_name(),
25 screen.variant_name()
26 );
27 }
28 self.screen = screen;
29 }
30
31 pub fn push_help_overlay(&mut self) {
35 if matches!(self.screen, Screen::Help { .. }) {
36 return;
37 }
38 log::debug!("[purple] screen: {} → Help", self.screen.variant_name());
39 let old = std::mem::replace(&mut self.screen, Screen::HostList);
40 self.screen = Screen::Help {
41 return_screen: Box::new(old),
42 };
43 }
44
45 pub fn pop_help_overlay(&mut self) {
49 let returned = {
50 let Screen::Help { return_screen } = &mut self.screen else {
51 return;
52 };
53 std::mem::replace(&mut **return_screen, Screen::HostList)
54 };
55 log::debug!("[purple] screen: Help → {}", returned.variant_name());
56 self.screen = returned;
57 }
58
59 pub fn cycle_top_page_next(&mut self) {
64 let old = self.top_page;
65 self.top_page = self.top_page.next();
66 log::debug!("[purple] top_page: {:?} → {:?} (Tab)", old, self.top_page);
67 }
68
69 pub fn cycle_top_page_prev(&mut self) {
71 let old = self.top_page;
72 self.top_page = self.top_page.prev();
73 log::debug!(
74 "[purple] top_page: {:?} → {:?} (Shift+Tab)",
75 old,
76 self.top_page
77 );
78 }
79
80 pub fn selected_host_index(&self) -> Option<usize> {
82 if self.search.query.is_some() {
83 let sel = self.ui.list_state.selected()?;
85 self.search.filtered_indices.get(sel).copied()
86 } else {
87 let sel = self.ui.list_state.selected()?;
89 match self.hosts_state.display_list.get(sel) {
90 Some(HostListItem::Host { index }) => Some(*index),
91 _ => None,
92 }
93 }
94 }
95
96 pub fn selected_host(&self) -> Option<&HostEntry> {
98 self.selected_host_index()
99 .and_then(|i| self.hosts_state.list.get(i))
100 }
101
102 pub fn selected_pattern(&self) -> Option<&PatternEntry> {
104 if self.search.query.is_some() {
105 let sel = self.ui.list_state.selected()?;
106 let host_count = self.search.filtered_indices.len();
107 if sel >= host_count {
108 let pattern_idx = sel - host_count;
109 return self
110 .search
111 .filtered_pattern_indices
112 .get(pattern_idx)
113 .and_then(|&i| self.hosts_state.patterns.get(i));
114 }
115 return None;
116 }
117 let sel = self.ui.list_state.selected()?;
118 match self.hosts_state.display_list.get(sel) {
119 Some(HostListItem::Pattern { index }) => self.hosts_state.patterns.get(*index),
120 _ => None,
121 }
122 }
123
124 pub fn is_pattern_selected(&self) -> bool {
126 if self.search.query.is_some() {
127 let Some(sel) = self.ui.list_state.selected() else {
128 return false;
129 };
130 let total =
131 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
132 return sel >= self.search.filtered_indices.len() && sel < total;
133 }
134 let Some(sel) = self.ui.list_state.selected() else {
135 return false;
136 };
137 matches!(
138 self.hosts_state.display_list.get(sel),
139 Some(HostListItem::Pattern { .. })
140 )
141 }
142
143 pub fn select_prev(&mut self) {
145 self.ui.detail_scroll = 0;
146 if self.search.query.is_some() {
147 let total =
148 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
149 super::cycle_selection(&mut self.ui.list_state, total, false);
150 } else {
151 self.select_prev_in_display_list();
152 }
153 }
154
155 pub fn select_next(&mut self) {
157 self.ui.detail_scroll = 0;
158 if self.search.query.is_some() {
159 let total =
160 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
161 super::cycle_selection(&mut self.ui.list_state, total, true);
162 } else {
163 self.select_next_in_display_list();
164 }
165 }
166
167 fn select_next_in_display_list(&mut self) {
168 if self.hosts_state.display_list.is_empty() {
169 return;
170 }
171 let len = self.hosts_state.display_list.len();
172 let current = self.ui.list_state.selected().unwrap_or(0);
173 for offset in 1..=len {
175 let idx = (current + offset) % len;
176 if matches!(
177 &self.hosts_state.display_list[idx],
178 HostListItem::Host { .. } | HostListItem::Pattern { .. }
179 ) {
180 self.ui.list_state.select(Some(idx));
181 return;
182 }
183 }
184 }
185
186 fn select_prev_in_display_list(&mut self) {
187 if self.hosts_state.display_list.is_empty() {
188 return;
189 }
190 let len = self.hosts_state.display_list.len();
191 let current = self.ui.list_state.selected().unwrap_or(0);
192 for offset in 1..=len {
194 let idx = (current + len - offset) % len;
195 if matches!(
196 &self.hosts_state.display_list[idx],
197 HostListItem::Host { .. } | HostListItem::Pattern { .. }
198 ) {
199 self.ui.list_state.select(Some(idx));
200 return;
201 }
202 }
203 }
204
205 pub fn page_down_host(&mut self) {
209 self.ui.detail_scroll = 0;
210 const PAGE_SIZE: usize = 10;
211 let before = self.ui.list_state.selected();
212 if self.search.query.is_some() {
213 let total =
214 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
215 super::page_down(&mut self.ui.list_state, total, PAGE_SIZE);
216 } else {
217 let current = self.ui.list_state.selected().unwrap_or(0);
218 let mut target = current;
219 let mut items_skipped = 0;
220 let len = self.hosts_state.display_list.len();
221 for i in (current + 1)..len {
222 if matches!(
223 self.hosts_state.display_list[i],
224 HostListItem::Host { .. } | HostListItem::Pattern { .. }
225 ) {
226 target = i;
227 items_skipped += 1;
228 if items_skipped >= PAGE_SIZE {
229 break;
230 }
231 }
232 }
233 self.ui.list_state.select(Some(target));
234 }
235 if self.ui.list_state.selected() == before {
237 self.select_next();
238 }
239 }
240
241 pub fn page_up_host(&mut self) {
245 self.ui.detail_scroll = 0;
246 const PAGE_SIZE: usize = 10;
247 let before = self.ui.list_state.selected();
248 if self.search.query.is_some() {
249 let total =
250 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
251 super::page_up(&mut self.ui.list_state, total, PAGE_SIZE);
252 } else {
253 let current = self.ui.list_state.selected().unwrap_or(0);
254 let mut target = current;
255 let mut items_skipped = 0;
256 for i in (0..current).rev() {
257 if matches!(
258 self.hosts_state.display_list[i],
259 HostListItem::Host { .. } | HostListItem::Pattern { .. }
260 ) {
261 target = i;
262 items_skipped += 1;
263 if items_skipped >= PAGE_SIZE {
264 break;
265 }
266 }
267 }
268 self.ui.list_state.select(Some(target));
269 }
270 if self.ui.list_state.selected() == before {
272 self.select_prev();
273 }
274 }
275 pub fn scan_keys(&mut self) {
276 let paths = self.env().paths().cloned();
277 let ssh_dir = paths.as_ref().map(crate::runtime::env::Paths::ssh_dir);
278 if let Some(ssh_dir) = ssh_dir {
279 self.keys.list = ssh_keys::discover_keys(
280 paths.as_ref(),
281 Path::new(&ssh_dir),
282 &self.hosts_state.list,
283 );
284 if !self.keys.list.is_empty() && self.keys.list_state.selected().is_none() {
285 self.keys.list_state.select(Some(0));
286 }
287 self.reload.keys_dir_mtime = crate::app::reload_state::get_mtime(&ssh_dir);
288 self.reload.key_file_mtimes =
289 crate::app::reload_state::snapshot_key_mtimes(&ssh_dir, &self.keys.list);
290 }
291 }
292
293 pub fn select_prev_key(&mut self) {
295 super::cycle_selection(&mut self.keys.list_state, self.keys.list.len(), false);
296 }
297
298 pub fn select_next_key(&mut self) {
300 super::cycle_selection(&mut self.keys.list_state, self.keys.list.len(), true);
301 }
302
303 pub fn select_prev_picker_key(&mut self) {
305 super::cycle_selection(&mut self.ui.key_picker.list, self.keys.list.len(), false);
306 }
307
308 pub fn select_next_picker_key(&mut self) {
310 super::cycle_selection(&mut self.ui.key_picker.list, self.keys.list.len(), true);
311 }
312
313 pub fn select_prev_password_source(&mut self) {
315 super::cycle_selection(
316 &mut self.ui.password_picker.list,
317 crate::askpass::PASSWORD_SOURCES.len(),
318 false,
319 );
320 }
321
322 pub fn select_next_password_source(&mut self) {
324 super::cycle_selection(
325 &mut self.ui.password_picker.list,
326 crate::askpass::PASSWORD_SOURCES.len(),
327 true,
328 );
329 }
330
331 pub fn proxyjump_candidates(&self) -> Vec<ProxyJumpCandidate> {
341 let editing_alias = match &self.screen {
342 Screen::EditHost { alias, .. } => Some(alias.as_str()),
343 _ => None,
344 };
345 let editing_hostname = match &self.screen {
346 Screen::EditHost { alias, .. } => self
347 .hosts_state
348 .list
349 .iter()
350 .find(|h| h.alias == *alias)
351 .map(|h| h.hostname.as_str()),
352 _ => None,
353 };
354 let editing_suffix = editing_hostname.and_then(domain_suffix);
355
356 let usage_counts = proxyjump_usage_counts(&self.hosts_state.list, editing_alias);
357 let mut scored = score_proxyjump_candidates(
358 &self.hosts_state.list,
359 editing_alias,
360 editing_suffix.as_deref(),
361 &usage_counts,
362 );
363
364 scored.sort_by(|(sa, a), (sb, b)| sb.cmp(sa).then_with(|| a.alias.cmp(&b.alias)));
366 let suggested: Vec<&HostEntry> = scored
367 .iter()
368 .filter(|(s, _)| *s > 0)
369 .take(3)
370 .map(|(_, h)| *h)
371 .collect();
372 let suggested_aliases: std::collections::HashSet<&str> =
373 suggested.iter().map(|h| h.alias.as_str()).collect();
374
375 scored.sort_by(|(_, a), (_, b)| a.alias.cmp(&b.alias));
377 let rest: Vec<&HostEntry> = scored
378 .into_iter()
379 .map(|(_, h)| h)
380 .filter(|h| !suggested_aliases.contains(h.alias.as_str()))
381 .collect();
382
383 build_proxyjump_items(&suggested, &rest)
384 }
385
386 pub fn proxyjump_first_host_index(&self) -> Option<usize> {
389 self.proxyjump_candidates()
390 .iter()
391 .position(|c| matches!(c, ProxyJumpCandidate::Host { .. }))
392 }
393
394 pub fn select_prev_proxyjump(&mut self) {
396 step_proxyjump_selection(self, false);
397 }
398
399 pub fn select_next_proxyjump(&mut self) {
401 step_proxyjump_selection(self, true);
402 }
403
404 pub fn vault_role_candidates(&self) -> Vec<String> {
406 let mut seen = std::collections::HashSet::new();
407 let mut roles = Vec::new();
408 for host in &self.hosts_state.list {
409 if let Some(ref role) = host.vault_ssh {
410 if seen.insert(role.clone()) {
411 roles.push(role.clone());
412 }
413 }
414 }
415 for section in &self.providers.config.sections {
417 let role = section.vault_role.trim();
418 if !role.is_empty() && seen.insert(role.to_string()) {
419 roles.push(role.to_string());
420 }
421 }
422 roles.sort();
423 roles
424 }
425
426 pub fn select_prev_vault_role(&mut self) {
428 let len = self.vault_role_candidates().len();
429 super::cycle_selection(&mut self.ui.vault_role_picker.list, len, false);
430 }
431
432 pub fn select_next_vault_role(&mut self) {
434 let len = self.vault_role_candidates().len();
435 super::cycle_selection(&mut self.ui.vault_role_picker.list, len, true);
436 }
437
438 pub fn collect_unique_tags(&self) -> Vec<String> {
440 let mut seen = std::collections::HashSet::new();
441 let mut tags = Vec::new();
442 let mut has_stale = false;
443 let mut has_vault_ssh = false;
444 let mut has_vault_kv = false;
445 for host in &self.hosts_state.list {
446 for tag in host.provider_tags.iter().chain(host.tags.iter()) {
447 if seen.insert(tag.clone()) {
448 tags.push(tag.clone());
449 }
450 }
451 if let Some(ref provider) = host.provider {
452 if seen.insert(provider.clone()) {
453 tags.push(provider.clone());
454 }
455 }
456 if host.stale.is_some() {
457 has_stale = true;
458 }
459 if crate::vault_ssh::resolve_vault_role(
460 host.vault_ssh.as_deref(),
461 host.provider.as_deref(),
462 host.provider_label.as_deref(),
463 &self.providers.config,
464 )
465 .is_some()
466 {
467 has_vault_ssh = true;
468 }
469 if host
470 .askpass
471 .as_deref()
472 .map(|s| s.starts_with("vault:"))
473 .unwrap_or(false)
474 {
475 has_vault_kv = true;
476 }
477 }
478 for pattern in &self.hosts_state.patterns {
479 for tag in &pattern.tags {
480 if seen.insert(tag.clone()) {
481 tags.push(tag.clone());
482 }
483 }
484 }
485 if has_stale && seen.insert("stale".to_string()) {
486 tags.push("stale".to_string());
487 }
488 if !has_vault_ssh {
489 for section in &self.providers.config.sections {
490 if !section.vault_role.is_empty() {
491 has_vault_ssh = true;
492 break;
493 }
494 }
495 }
496 if has_vault_ssh && seen.insert("vault-ssh".to_string()) {
497 tags.push("vault-ssh".to_string());
498 }
499 if has_vault_kv && seen.insert("vault-kv".to_string()) {
500 tags.push("vault-kv".to_string());
501 }
502 tags.sort_by_cached_key(|a| a.to_lowercase());
503 tags
504 }
505
506 pub fn open_bulk_tag_editor(&mut self) -> bool {
515 let mut aliases: Vec<String> = Vec::new();
516 let mut skipped: Vec<String> = Vec::new();
517 let mut alias_set: std::collections::HashSet<String> = std::collections::HashSet::new();
518 for &idx in &self.hosts_state.multi_select {
519 if let Some(host) = self.hosts_state.list.get(idx) {
520 if !alias_set.insert(host.alias.clone()) {
521 continue;
522 }
523 if host.source_file.is_some() {
524 skipped.push(host.alias.clone());
525 }
526 aliases.push(host.alias.clone());
527 }
528 }
529 if aliases.is_empty() {
530 return false;
531 }
532 aliases.sort();
533 skipped.sort();
534
535 let mut candidate_tags: std::collections::BTreeSet<String> =
540 std::collections::BTreeSet::new();
541 for host in &self.hosts_state.list {
542 for tag in &host.tags {
543 candidate_tags.insert(tag.clone());
544 }
545 }
546 for pattern in &self.hosts_state.patterns {
547 for tag in &pattern.tags {
548 candidate_tags.insert(tag.clone());
549 }
550 }
551
552 let selected_set: std::collections::HashSet<&str> =
553 aliases.iter().map(|s| s.as_str()).collect();
554 let rows: Vec<BulkTagRow> = candidate_tags
555 .into_iter()
556 .map(|tag| {
557 let initial_count = self
558 .hosts_state
559 .list
560 .iter()
561 .filter(|h| selected_set.contains(h.alias.as_str()))
562 .filter(|h| h.tags.iter().any(|t| t == &tag))
563 .count();
564 BulkTagRow {
565 tag,
566 initial_count,
567 action: BulkTagAction::Leave,
568 }
569 })
570 .collect();
571
572 let initial_actions: Vec<BulkTagAction> = rows.iter().map(|r| r.action).collect();
576 self.forms.bulk_tag_editor = BulkTagEditorState {
577 rows,
578 aliases,
579 skipped_included: skipped,
580 new_tag_input: None,
581 new_tag_cursor: 0,
582 initial_actions,
583 };
584 self.ui.bulk_tag_editor_state = ListState::default();
585 if !self.forms.bulk_tag_editor.rows.is_empty() {
586 self.ui.bulk_tag_editor_state.select(Some(0));
587 }
588 self.set_screen(Screen::BulkTagEditor);
589 true
590 }
591
592 pub fn bulk_tag_editor_next(&mut self) {
594 super::cycle_selection(
595 &mut self.ui.bulk_tag_editor_state,
596 self.forms.bulk_tag_editor.rows.len(),
597 true,
598 );
599 }
600
601 pub fn bulk_tag_editor_prev(&mut self) {
603 super::cycle_selection(
604 &mut self.ui.bulk_tag_editor_state,
605 self.forms.bulk_tag_editor.rows.len(),
606 false,
607 );
608 }
609
610 pub fn bulk_tag_editor_cycle_current(&mut self) {
613 super::bulk_tag_cycle_current(&self.ui, &mut self.forms);
614 }
615
616 pub fn bulk_tag_editor_commit_new_tag(&mut self) {
621 super::bulk_tag_commit_new_tag(&mut self.ui, &mut self.forms);
622 }
623
624 pub fn bulk_tag_apply(&mut self) -> Result<BulkTagApplyResult, String> {
629 let result = super::apply_bulk_tags(&mut self.hosts_state, &mut self.forms)?;
634 if result.changed_hosts > 0 {
635 self.update_last_modified();
636 self.reload_hosts();
637 }
638 Ok(result)
639 }
640
641 pub fn open_tag_picker(&mut self) {
643 self.tags.list = self.collect_unique_tags();
644 self.ui.tag_picker_state = ListState::default();
645 if !self.tags.list.is_empty() {
646 self.ui.tag_picker_state.select(Some(0));
647 }
648 self.set_screen(Screen::TagPicker);
649 }
650
651 pub fn select_prev_tag(&mut self) {
653 super::cycle_selection(&mut self.ui.tag_picker_state, self.tags.list.len(), false);
654 }
655
656 pub fn select_next_tag(&mut self) {
658 super::cycle_selection(&mut self.ui.tag_picker_state, self.tags.list.len(), true);
659 }
660
661 pub fn refresh_tunnel_list(&mut self, alias: &str) {
664 self.tunnels
665 .load_directives(&self.hosts_state.ssh_config, alias);
666 }
667
668 pub fn select_prev_tunnel(&mut self) {
670 super::cycle_selection(
671 &mut self.ui.tunnel_list_state,
672 self.tunnels.list.len(),
673 false,
674 );
675 }
676
677 pub fn select_next_tunnel(&mut self) {
679 super::cycle_selection(
680 &mut self.ui.tunnel_list_state,
681 self.tunnels.list.len(),
682 true,
683 );
684 }
685
686 pub fn select_prev_snippet(&mut self) {
688 super::cycle_selection(
689 &mut self.ui.snippet_picker_state,
690 self.snippets.store.snippets.len(),
691 false,
692 );
693 }
694
695 pub fn select_next_snippet(&mut self) {
697 super::cycle_selection(
698 &mut self.ui.snippet_picker_state,
699 self.snippets.store.snippets.len(),
700 true,
701 );
702 }
703}
704
705const JUMP_KEYWORDS: &[&str] = &["jump", "bastion", "gateway", "proxy", "gw"];
706
707fn proxyjump_usage_counts(
710 hosts: &[HostEntry],
711 editing_alias: Option<&str>,
712) -> std::collections::HashMap<String, u32> {
713 let mut counts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
714 for h in hosts {
715 if h.proxy_jump.is_empty() || editing_alias == Some(h.alias.as_str()) {
716 continue;
717 }
718 for hop in parse_proxy_jump_hops(&h.proxy_jump) {
719 *counts.entry(hop).or_insert(0) += 1;
720 }
721 }
722 counts
723}
724
725fn score_proxyjump_candidates<'a>(
728 hosts: &'a [HostEntry],
729 editing_alias: Option<&str>,
730 editing_suffix: Option<&str>,
731 usage_counts: &std::collections::HashMap<String, u32>,
732) -> Vec<(u32, &'a HostEntry)> {
733 hosts
734 .iter()
735 .filter(|h| editing_alias.is_none_or(|a| h.alias != a))
736 .map(|h| {
737 let usage = usage_counts.get(&h.alias).copied().unwrap_or(0);
738 let kw = has_jump_keyword(&h.alias, &h.hostname);
739 let same = editing_suffix
740 .and_then(|suf| domain_suffix(&h.hostname).map(|s| s == suf))
741 .unwrap_or(false);
742 let score = usage * 10 + u32::from(kw) * 5 + u32::from(same) * 3;
743 (score, h)
744 })
745 .collect()
746}
747
748fn build_proxyjump_items(suggested: &[&HostEntry], rest: &[&HostEntry]) -> Vec<ProxyJumpCandidate> {
752 let mut items = Vec::with_capacity(suggested.len() + rest.len() + 2);
753 if !suggested.is_empty() {
754 items.push(ProxyJumpCandidate::SectionLabel("Suggestions"));
755 }
756 for h in suggested {
757 items.push(ProxyJumpCandidate::Host {
758 alias: h.alias.clone(),
759 hostname: h.hostname.clone(),
760 suggested: true,
761 });
762 }
763 if !suggested.is_empty() && !rest.is_empty() {
764 items.push(ProxyJumpCandidate::Separator);
765 }
766 for h in rest {
767 items.push(ProxyJumpCandidate::Host {
768 alias: h.alias.clone(),
769 hostname: h.hostname.clone(),
770 suggested: false,
771 });
772 }
773 items
774}
775
776pub(crate) fn parse_proxy_jump_hops(proxy_jump: &str) -> Vec<String> {
782 proxy_jump
783 .split(',')
784 .filter_map(|hop| {
785 let h = hop.trim();
786 if h.is_empty() {
787 return None;
788 }
789 let h = h.split_once('@').map_or(h, |(_, host)| host);
790 let h = if let Some(bracketed) = h.strip_prefix('[') {
791 let (inner, _) = bracketed.split_once(']')?;
792 inner
793 } else {
794 h.rsplit_once(':').map_or(h, |(host, _)| host)
795 };
796 if h.is_empty() {
797 None
798 } else {
799 Some(h.to_string())
800 }
801 })
802 .collect()
803}
804
805pub(crate) fn has_jump_keyword(alias: &str, hostname: &str) -> bool {
808 let a = alias.to_ascii_lowercase();
809 let h = hostname.to_ascii_lowercase();
810 JUMP_KEYWORDS
811 .iter()
812 .any(|kw| a.contains(kw) || h.contains(kw))
813}
814
815pub(crate) fn domain_suffix(hostname: &str) -> Option<String> {
822 let h = hostname.trim();
823 if h.is_empty() || h.starts_with('[') {
824 return None;
825 }
826 if h.parse::<std::net::IpAddr>().is_ok() {
827 return None;
828 }
829 let labels: Vec<&str> = h.split('.').collect();
830 if labels.len() < 2 {
831 return None;
832 }
833 let mut end = labels.len();
836 while end > 0 && labels[end - 1].is_empty() {
837 end -= 1;
838 }
839 if end < 2 {
840 return None;
841 }
842 let tail = &labels[end - 2..end];
843 Some(tail.join(".").to_ascii_lowercase())
844}
845
846fn step_proxyjump_selection(app: &mut App, forward: bool) {
852 let candidates = app.proxyjump_candidates();
853 let len = candidates.len();
854 if len == 0 {
855 app.ui.proxyjump_picker.list.select(None);
856 return;
857 }
858 let seed: usize = match app.ui.proxyjump_picker.list.selected() {
863 Some(idx) => idx,
864 None if forward => len - 1,
865 None => 0,
866 };
867 let mut next = seed;
868 for _ in 0..len {
869 next = if forward {
870 (next + 1) % len
871 } else {
872 (next + len - 1) % len
873 };
874 if matches!(candidates.get(next), Some(ProxyJumpCandidate::Host { .. })) {
875 app.ui.proxyjump_picker.list.select(Some(next));
876 return;
877 }
878 }
879}