Skip to main content

netget/tui/modal/
protocol_picker.rs

1//! Protocol picker for `+ new server or client`: every server and client
2//! protocol in one filterable list with maturity badges and a preview of what
3//! the protocol is and needs. `http server` and `http client` sit next to
4//! each other, so there is no kind to choose before choosing.
5
6use crate::privilege::SystemCapabilities;
7use crate::protocol::metadata::DevelopmentState;
8use crate::tui::app::Section;
9
10#[derive(Debug, Clone)]
11pub struct ProtocolEntry {
12    /// Server or client.
13    pub kind: Section,
14    pub name: String,
15    pub description: String,
16    pub state: DevelopmentState,
17    pub notes: Option<String>,
18    /// The port a server starts on when none is given, and why — resolved by
19    /// `protocol::default_port` against this process's privilege and what is already bound.
20    /// `None` for a client, and for a server that binds no port at all (an interface, a pipe).
21    pub default_port: Option<crate::protocol::default_port::DefaultPort>,
22    /// Set when this build cannot satisfy the protocol's privilege
23    /// requirement; the entry stays listed but says why it will refuse.
24    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
45/// All protocols available for a section, sorted by name. The client registry
46/// returns names unsorted, so both are sorted here.
47pub 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
106/// Both kinds together, sorted by name with the server before the client of
107/// the same protocol.
108pub 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
120/// Case-insensitive filter over name, kind and description, ranked by how
121/// well the match fits the name.
122///
123/// Ranking matters once every protocol is compiled in: typing "tcp" matches
124/// Modbus, MQTT and a dozen others whose *description* mentions TCP, and an
125/// alphabetical list puts one of those first. Someone typing "tcp" wants TCP.
126/// Words after the first narrow further — `http client` is the http client.
127pub 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    // Stable sort keeps the alphabetical order within each rank.
160    matches.sort_by_key(|(rank, _)| *rank);
161    matches.into_iter().map(|(_, entry)| entry).collect()
162}