1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Clone wizard (`:clone`, plans/0004 §2): three screens — repo
//! checkboxes, local destination mini-browser, summary. Mock stage:
//! no git runs; the summary shows what *would* happen. Esc anywhere
//! closes the whole wizard (no partial state).
//!
//! Layout: this file is the state + public surface; `keys.rs` handles
//! input, `render.rs` draws.
use super::vim_input::VimInput;
use crate::provider::RepoInfo;
use std::path::PathBuf;
mod keys;
mod render;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Screen {
Repos,
Destination,
Summary,
}
/// List or the button row owns the keyboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Focus {
List,
Buttons,
}
pub struct CloneWizard {
screen: Screen,
/// Resolved repos (files already folded to their repo upstream),
/// with v1.4 listing metadata when the provider reported it
/// (plans/0014 #1): recently pushed first, undated by name.
repos: Vec<(RepoInfo, bool)>,
cursor: usize,
focus: Focus,
/// Button row cursor: 0 = back, 1 = next/clone.
button: usize,
/// Destination browser state (screen 2).
dest: PathBuf,
dest_entries: Vec<String>,
dest_cursor: usize,
/// Line scroll offset of the current screen's content area.
scroll: usize,
/// Transient `/` filter (house style: every scanned list filters).
filter: VimInput,
filtering: bool,
pre_filter: String,
filter_value: String,
}
impl CloneWizard {
pub fn new(repos: Vec<RepoInfo>, start: PathBuf) -> Self {
// v1.4 (plans/0014 #1): sort by pushed_at desc — ISO-8601
// sorts lexicographically; undated entries fall back to name
// order (providers that send bare names keep the old order).
let mut repos = repos;
repos.sort_by(|a, b| {
b.pushed_at
.cmp(&a.pushed_at)
.then_with(|| a.name.cmp(&b.name))
});
let repos = repos.into_iter().map(|r| (r, true)).collect();
let mut wizard = CloneWizard {
screen: Screen::Repos,
repos,
cursor: 0,
focus: Focus::List,
button: 1, // next is the default action
dest: start,
dest_entries: vec![],
dest_cursor: 0,
scroll: 0,
filter: VimInput::transient(),
filtering: false,
pre_filter: String::new(),
filter_value: String::new(),
};
wizard.refresh_dest();
wizard
}
/// Local dirs of the current destination path, `..` first.
fn refresh_dest(&mut self) {
let mut entries = vec!["..".to_string()];
if let Ok(read) = std::fs::read_dir(&self.dest) {
let mut dirs: Vec<String> = read
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.filter_map(|e| e.file_name().into_string().ok())
.filter(|name| !name.starts_with('.')) // no dotdirs
.collect();
dirs.sort();
entries.extend(dirs);
}
self.dest_entries = entries;
self.dest_cursor = 0;
}
fn checked(&self) -> impl Iterator<Item = &RepoInfo> {
self.repos.iter().filter(|(_, on)| *on).map(|(r, _)| r)
}
/// Repo indices surviving the committed filter.
fn visible_repos(&self) -> Vec<usize> {
let needle = self.filter_value.to_lowercase();
self.repos
.iter()
.enumerate()
.filter(|(_, (r, _))| needle.is_empty() || r.name.to_lowercase().contains(&needle))
.map(|(i, _)| i)
.collect()
}
/// Destination indices surviving the committed filter.
fn visible_dest(&self) -> Vec<usize> {
let needle = self.filter_value.to_lowercase();
self.dest_entries
.iter()
.enumerate()
.filter(|(_, e)| needle.is_empty() || e.to_lowercase().contains(&needle))
.map(|(i, _)| i)
.collect()
}
/// Clone target: <dest>/<org>/<repo> — the org level prevents
/// same-name collisions between orgs.
fn target(&self, repo: &str) -> std::path::PathBuf {
self.dest.join(repo)
}
/// Button labels: arrows mark direction of travel (`← Back`,
/// `Next →` — the standard wizard convention) and the committing
/// action names its key (`⏎ Clone`). The glyphs ship in both the
/// vendored Nerd Font Mono and plain JetBrains Mono.
fn buttons(&self) -> (&'static str, &'static str) {
match self.screen {
Screen::Repos | Screen::Destination => ("← Back", "Next →"),
Screen::Summary => ("← Back", "⏎ Clone"),
}
}
}