1pub const MAX_TUI_NODES: usize = 500;
11
12pub const DEFAULT_VISIBLE_NODES: usize = 20;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum NodeShape {
18 #[default]
20 Circle,
21 Diamond,
23 Square,
25 Triangle,
27 Star,
29}
30
31impl NodeShape {
32 #[must_use]
34 pub fn unicode(&self) -> char {
35 match self {
36 Self::Circle => '●',
37 Self::Diamond => '◆',
38 Self::Square => '■',
39 Self::Triangle => '▲',
40 Self::Star => '★',
41 }
42 }
43
44 #[must_use]
46 pub fn ascii(&self) -> char {
47 match self {
48 Self::Circle => 'o',
49 Self::Diamond => '<',
50 Self::Square => '#',
51 Self::Triangle => '^',
52 Self::Star => '*',
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub enum NodeStatus {
60 #[default]
62 Healthy,
63 Warning,
65 Error,
67 Info,
69 Neutral,
71}
72
73impl NodeStatus {
74 #[must_use]
76 pub fn shape(&self) -> NodeShape {
77 match self {
78 Self::Healthy => NodeShape::Circle,
79 Self::Warning => NodeShape::Triangle,
80 Self::Error => NodeShape::Diamond,
81 Self::Info => NodeShape::Star,
82 Self::Neutral => NodeShape::Square,
83 }
84 }
85
86 #[must_use]
88 pub fn color_code(&self) -> &'static str {
89 match self {
90 Self::Healthy => "\x1b[32m", Self::Warning => "\x1b[33m", Self::Error => "\x1b[31m", Self::Info => "\x1b[36m", Self::Neutral => "\x1b[90m", }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Default)]
101pub struct Position {
102 pub x: f32,
103 pub y: f32,
104}
105
106impl Position {
107 #[must_use]
109 pub fn new(x: f32, y: f32) -> Self {
110 Self { x, y }
111 }
112
113 #[must_use]
115 pub fn distance(&self, other: &Self) -> f32 {
116 let dx = self.x - other.x;
117 let dy = self.y - other.y;
118 (dx * dx + dy * dy).sqrt()
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct Node<T> {
125 pub id: String,
127 pub data: T,
129 pub status: NodeStatus,
131 pub label: Option<String>,
133 pub position: Position,
135 pub importance: f32,
137}
138
139impl<T> Node<T> {
140 pub fn new(id: impl Into<String>, data: T) -> Self {
142 Self {
143 id: id.into(),
144 data,
145 status: NodeStatus::default(),
146 label: None,
147 position: Position::default(),
148 importance: 1.0,
149 }
150 }
151
152 #[must_use]
154 pub fn with_status(mut self, status: NodeStatus) -> Self {
155 self.status = status;
156 self
157 }
158
159 #[must_use]
161 pub fn with_label(mut self, label: impl Into<String>) -> Self {
162 self.label = Some(label.into());
163 self
164 }
165
166 #[must_use]
168 pub fn with_importance(mut self, importance: f32) -> Self {
169 self.importance = importance;
170 self
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct Edge<E> {
177 pub from: String,
179 pub to: String,
181 pub data: E,
183 pub weight: f32,
185 pub label: Option<String>,
187}
188
189impl<E> Edge<E> {
190 pub fn new(from: impl Into<String>, to: impl Into<String>, data: E) -> Self {
192 Self { from: from.into(), to: to.into(), data, weight: 1.0, label: None }
193 }
194
195 #[must_use]
197 pub fn with_weight(mut self, weight: f32) -> Self {
198 self.weight = weight;
199 self
200 }
201
202 #[must_use]
204 pub fn with_label(mut self, label: impl Into<String>) -> Self {
205 self.label = Some(label.into());
206 self
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
219 fn test_node_shape_default() {
220 assert_eq!(NodeShape::default(), NodeShape::Circle);
221 }
222
223 #[test]
224 fn test_node_shape_unicode() {
225 assert_eq!(NodeShape::Circle.unicode(), '●');
226 assert_eq!(NodeShape::Diamond.unicode(), '◆');
227 assert_eq!(NodeShape::Square.unicode(), '■');
228 assert_eq!(NodeShape::Triangle.unicode(), '▲');
229 assert_eq!(NodeShape::Star.unicode(), '★');
230 }
231
232 #[test]
233 fn test_node_shape_ascii() {
234 assert_eq!(NodeShape::Circle.ascii(), 'o');
235 assert_eq!(NodeShape::Diamond.ascii(), '<');
236 assert_eq!(NodeShape::Square.ascii(), '#');
237 assert_eq!(NodeShape::Triangle.ascii(), '^');
238 assert_eq!(NodeShape::Star.ascii(), '*');
239 }
240
241 #[test]
246 fn test_node_status_default() {
247 assert_eq!(NodeStatus::default(), NodeStatus::Healthy);
248 }
249
250 #[test]
251 fn test_node_status_shape() {
252 assert_eq!(NodeStatus::Healthy.shape(), NodeShape::Circle);
253 assert_eq!(NodeStatus::Warning.shape(), NodeShape::Triangle);
254 assert_eq!(NodeStatus::Error.shape(), NodeShape::Diamond);
255 assert_eq!(NodeStatus::Info.shape(), NodeShape::Star);
256 assert_eq!(NodeStatus::Neutral.shape(), NodeShape::Square);
257 }
258
259 #[test]
260 fn test_node_status_color_code() {
261 assert!(NodeStatus::Healthy.color_code().contains("32m"));
262 assert!(NodeStatus::Warning.color_code().contains("33m"));
263 assert!(NodeStatus::Error.color_code().contains("31m"));
264 assert!(NodeStatus::Info.color_code().contains("36m"));
265 assert!(NodeStatus::Neutral.color_code().contains("90m"));
266 }
267
268 #[test]
273 fn test_position_default() {
274 let pos = Position::default();
275 assert_eq!(pos.x, 0.0);
276 assert_eq!(pos.y, 0.0);
277 }
278
279 #[test]
280 fn test_position_new() {
281 let pos = Position::new(10.0, 20.0);
282 assert_eq!(pos.x, 10.0);
283 assert_eq!(pos.y, 20.0);
284 }
285
286 #[test]
287 fn test_position_distance() {
288 let p1 = Position::new(0.0, 0.0);
289 let p2 = Position::new(3.0, 4.0);
290 assert!((p1.distance(&p2) - 5.0).abs() < 0.001);
291 }
292
293 #[test]
294 fn test_position_distance_to_self() {
295 let p = Position::new(5.0, 5.0);
296 assert!((p.distance(&p)).abs() < 0.001);
297 }
298
299 #[test]
304 fn test_node_new() {
305 let node = Node::new("test", 42);
306 assert_eq!(node.id, "test");
307 assert_eq!(node.data, 42);
308 assert_eq!(node.status, NodeStatus::Healthy);
309 assert!(node.label.is_none());
310 assert_eq!(node.importance, 1.0);
311 }
312
313 #[test]
314 fn test_node_with_status() {
315 let node = Node::new("test", ()).with_status(NodeStatus::Error);
316 assert_eq!(node.status, NodeStatus::Error);
317 }
318
319 #[test]
320 fn test_node_with_label() {
321 let node = Node::new("test", ()).with_label("My Label");
322 assert_eq!(node.label, Some("My Label".to_string()));
323 }
324
325 #[test]
326 fn test_node_with_importance() {
327 let node = Node::new("test", ()).with_importance(0.5);
328 assert_eq!(node.importance, 0.5);
329 }
330
331 #[test]
332 fn test_node_builder_chain() {
333 let node = Node::new("test", "data")
334 .with_status(NodeStatus::Warning)
335 .with_label("Label")
336 .with_importance(0.75);
337 assert_eq!(node.status, NodeStatus::Warning);
338 assert_eq!(node.label, Some("Label".to_string()));
339 assert_eq!(node.importance, 0.75);
340 }
341
342 #[test]
347 fn test_edge_new() {
348 let edge = Edge::new("A", "B", 100);
349 assert_eq!(edge.from, "A");
350 assert_eq!(edge.to, "B");
351 assert_eq!(edge.data, 100);
352 assert_eq!(edge.weight, 1.0);
353 assert!(edge.label.is_none());
354 }
355
356 #[test]
357 fn test_edge_with_weight() {
358 let edge = Edge::new("A", "B", ()).with_weight(2.5);
359 assert_eq!(edge.weight, 2.5);
360 }
361
362 #[test]
363 fn test_edge_with_label() {
364 let edge = Edge::new("A", "B", ()).with_label("depends_on");
365 assert_eq!(edge.label, Some("depends_on".to_string()));
366 }
367
368 #[test]
369 fn test_edge_builder_chain() {
370 let edge =
371 Edge::new("source", "target", "edge_data").with_weight(3.0).with_label("relation");
372 assert_eq!(edge.from, "source");
373 assert_eq!(edge.to, "target");
374 assert_eq!(edge.weight, 3.0);
375 assert_eq!(edge.label, Some("relation".to_string()));
376 }
377
378 #[test]
383 fn test_constants() {
384 assert_eq!(MAX_TUI_NODES, 500);
385 assert_eq!(DEFAULT_VISIBLE_NODES, 20);
386 }
387}