netget/tui/modal/
protocol_picker.rs1use crate::privilege::SystemCapabilities;
7use crate::protocol::metadata::DevelopmentState;
8use crate::tui::app::Section;
9
10#[derive(Debug, Clone)]
11pub struct ProtocolEntry {
12 pub kind: Section,
14 pub name: String,
15 pub description: String,
16 pub state: DevelopmentState,
17 pub notes: Option<String>,
18 pub default_port: Option<crate::protocol::default_port::DefaultPort>,
22 pub privilege_note: Option<String>,
25}
26
27impl ProtocolEntry {
28 pub fn kind_label(&self) -> &'static str {
29 match self.kind {
30 Section::Servers => "server",
31 Section::Clients => "client",
32 }
33 }
34
35 pub fn badge(&self) -> &'static str {
36 match self.state {
37 DevelopmentState::Stable => "[stable]",
38 DevelopmentState::Beta => "[beta]",
39 DevelopmentState::Experimental => "[exp]",
40 DevelopmentState::Incomplete => "[incomplete]",
41 }
42 }
43}
44
45pub fn entries(section: Section, caps: &SystemCapabilities) -> Vec<ProtocolEntry> {
48 let mut entries = match section {
49 Section::Servers => {
50 let registry = crate::protocol::server_registry::registry();
51 registry
52 .available_protocols()
53 .into_iter()
54 .filter_map(|name| {
55 let protocol = registry.get(name)?;
56 let metadata = protocol.metadata();
57 let privilege_note = if metadata.privilege_requirement.is_met_by(caps) {
58 None
59 } else {
60 Some(format!(
61 "needs {:?} — will refuse to start in this process",
62 metadata.privilege_requirement
63 ))
64 };
65 Some(ProtocolEntry {
66 kind: Section::Servers,
67 name: name.to_string(),
68 description: protocol.description().to_string(),
69 state: metadata.state,
70 notes: metadata.notes.map(|n| n.to_string()),
71 default_port: crate::protocol::default_port::default_port_for_server(
72 protocol.as_ref(),
73 None,
74 caps,
75 ),
76 privilege_note,
77 })
78 })
79 .collect::<Vec<_>>()
80 }
81 Section::Clients => {
82 let registry = &crate::protocol::CLIENT_REGISTRY;
83 registry
84 .list_protocols()
85 .into_iter()
86 .filter_map(|name| {
87 let protocol = registry.get(&name)?;
88 let metadata = protocol.metadata();
89 Some(ProtocolEntry {
90 kind: Section::Clients,
91 name: name.clone(),
92 description: protocol.description().to_string(),
93 state: metadata.state,
94 notes: metadata.notes.map(|n| n.to_string()),
95 default_port: None,
96 privilege_note: None,
97 })
98 })
99 .collect::<Vec<_>>()
100 }
101 };
102 entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
103 entries
104}
105
106pub fn all_entries(caps: &SystemCapabilities) -> Vec<ProtocolEntry> {
109 let mut all = entries(Section::Servers, caps);
110 all.extend(entries(Section::Clients, caps));
111 all.sort_by(|a, b| {
112 a.name
113 .to_lowercase()
114 .cmp(&b.name.to_lowercase())
115 .then_with(|| (a.kind == Section::Clients).cmp(&(b.kind == Section::Clients)))
116 });
117 all
118}
119
120pub fn filter<'a>(entries: &'a [ProtocolEntry], needle: &str) -> Vec<&'a ProtocolEntry> {
128 let needle = needle.to_lowercase();
129 let mut words = needle.split_whitespace();
130 let Some(first) = words.next() else {
131 return entries.iter().collect();
132 };
133 let rest: Vec<&str> = words.collect();
134
135 let mut matches: Vec<(u8, &ProtocolEntry)> = entries
136 .iter()
137 .filter_map(|entry| {
138 let name = entry.name.to_lowercase();
139 let kind = entry.kind_label();
140 let description = entry.description.to_lowercase();
141 let rank = if name == first || (kind.starts_with(first) && rest.is_empty()) {
142 0
143 } else if name.starts_with(first) {
144 1
145 } else if name.contains(first) {
146 2
147 } else if description.contains(first) {
148 3
149 } else {
150 return None;
151 };
152 let narrowed = rest.iter().all(|word| {
153 name.contains(word) || kind.starts_with(word) || description.contains(word)
154 });
155 narrowed.then_some((rank, entry))
156 })
157 .collect();
158
159 matches.sort_by_key(|(rank, _)| *rank);
161 matches.into_iter().map(|(_, entry)| entry).collect()
162}