Skip to main content

graphs_tui/
types.rs

1use std::collections::HashMap;
2use std::fmt;
3
4/// Node identifier type
5pub type NodeId = String;
6
7/// Flow direction for the diagram
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Direction {
10    /// Left to Right
11    LR,
12    /// Right to Left
13    RL,
14    /// Top to Bottom
15    TB,
16    /// Bottom to Top
17    BT,
18}
19
20impl Direction {
21    /// Parse direction from string
22    pub fn parse(s: &str) -> Option<Direction> {
23        match s.to_uppercase().as_str() {
24            "LR" => Some(Direction::LR),
25            "RL" => Some(Direction::RL),
26            "TB" | "TD" => Some(Direction::TB),
27            "BT" => Some(Direction::BT),
28            _ => None,
29        }
30    }
31
32    /// Check if direction is horizontal (LR or RL)
33    pub fn is_horizontal(&self) -> bool {
34        matches!(self, Direction::LR | Direction::RL)
35    }
36}
37
38/// Shape of a node
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum NodeShape {
41    /// Rectangle [Label]
42    #[default]
43    Rectangle,
44    /// Rounded rectangle (Label)
45    Rounded,
46    /// Circle ((Label))
47    Circle,
48    /// Diamond/rhombus {Label}
49    Diamond,
50    /// Cylinder/database [(Label)]
51    Cylinder,
52    /// Stadium shape ([Label])
53    Stadium,
54    /// Subroutine [[Label]]
55    Subroutine,
56    /// Hexagon {{Label}}
57    Hexagon,
58    /// Parallelogram [/Label/]
59    Parallelogram,
60    /// Reverse Parallelogram [\Label\]
61    ParallelogramAlt,
62    /// Trapezoid [/Label\]
63    Trapezoid,
64    /// Reverse Trapezoid [\Label/]
65    TrapezoidAlt,
66    /// Table (D2 sql_table)
67    Table,
68    /// Person (D2 stick figure)
69    Person,
70    /// Cloud (D2 bumpy border)
71    Cloud,
72    /// Document/page (D2 wavy bottom)
73    Document,
74}
75
76/// Style of an edge/link
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub enum EdgeStyle {
79    /// Solid arrow -->
80    #[default]
81    Arrow,
82    /// Solid line ---
83    Line,
84    /// Dotted arrow -.->
85    DottedArrow,
86    /// Dotted line -.-
87    DottedLine,
88    /// Thick arrow ==>
89    ThickArrow,
90    /// Thick line ===
91    ThickLine,
92}
93
94/// A field inside a sql_table or class node (D2)
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct TableField {
97    pub name: String,
98    pub type_info: Option<String>,
99    pub constraint: Option<String>,
100}
101
102/// A subgraph/group of nodes
103#[derive(Debug, Clone)]
104pub struct Subgraph {
105    pub id: String,
106    pub label: String,
107    pub nodes: Vec<NodeId>,
108    pub parent: Option<String>,
109    pub x: usize,
110    pub y: usize,
111    pub width: usize,
112    pub height: usize,
113}
114
115impl Subgraph {
116    pub fn new(id: String, label: String) -> Self {
117        Self {
118            id,
119            label,
120            nodes: Vec::new(),
121            parent: None,
122            x: 0,
123            y: 0,
124            width: 0,
125            height: 0,
126        }
127    }
128}
129
130/// ANSI color for styling
131#[derive(Debug, Clone, Default)]
132pub struct NodeStyle {
133    /// Foreground color (ANSI escape code)
134    pub color: Option<String>,
135}
136
137/// A node in the flowchart
138#[derive(Debug, Clone)]
139pub struct Node {
140    pub id: NodeId,
141    pub label: String,
142    pub shape: NodeShape,
143    pub subgraph: Option<String>,
144    pub fields: Vec<TableField>,
145    pub width: usize,
146    pub height: usize,
147    pub x: usize,
148    pub y: usize,
149    /// Style class name applied to this node
150    pub style_class: Option<String>,
151}
152
153impl Node {
154    /// Create a new node with given id and label
155    pub fn new(id: NodeId, label: String) -> Self {
156        Self {
157            id,
158            label,
159            shape: NodeShape::default(),
160            subgraph: None,
161            fields: Vec::new(),
162            width: 0,
163            height: 0,
164            x: 0,
165            y: 0,
166            style_class: None,
167        }
168    }
169
170    /// Create a new node with shape
171    pub fn with_shape(id: NodeId, label: String, shape: NodeShape) -> Self {
172        Self {
173            id,
174            label,
175            shape,
176            subgraph: None,
177            fields: Vec::new(),
178            width: 0,
179            height: 0,
180            x: 0,
181            y: 0,
182            style_class: None,
183        }
184    }
185}
186
187/// An edge connecting two nodes
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct Edge {
190    pub from: NodeId,
191    pub to: NodeId,
192    pub label: Option<String>,
193    pub style: EdgeStyle,
194}
195
196/// The complete graph structure
197#[derive(Debug, Clone)]
198pub struct Graph {
199    pub direction: Direction,
200    pub nodes: HashMap<NodeId, Node>,
201    pub edges: Vec<Edge>,
202    pub subgraphs: Vec<Subgraph>,
203    /// Style class definitions (classDef name color:#hex)
204    pub style_classes: HashMap<String, NodeStyle>,
205}
206
207impl Graph {
208    /// Create a new empty graph with given direction
209    pub fn new(direction: Direction) -> Self {
210        Self {
211            direction,
212            nodes: HashMap::new(),
213            edges: Vec::new(),
214            subgraphs: Vec::new(),
215            style_classes: HashMap::new(),
216        }
217    }
218}
219
220/// Options for rendering the diagram
221#[derive(Debug, Clone)]
222pub struct RenderOptions {
223    /// Use ASCII characters instead of Unicode
224    pub ascii: bool,
225    /// Maximum width constraint for the diagram
226    pub max_width: Option<usize>,
227    /// Horizontal gap between nodes (default: 8)
228    pub padding_x: usize,
229    /// Vertical gap between nodes (default: 4)
230    pub padding_y: usize,
231    /// Padding between text and node border (default: 1)
232    pub border_padding: usize,
233    /// Enable ANSI color output (default: false)
234    pub colors: bool,
235}
236
237impl Default for RenderOptions {
238    fn default() -> Self {
239        Self {
240            ascii: false,
241            max_width: None,
242            padding_x: 8,
243            padding_y: 4,
244            border_padding: 1,
245            colors: false,
246        }
247    }
248}
249
250/// Structured warning emitted during layout or rendering
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub enum DiagramWarning {
253    /// A cycle was detected involving the listed nodes
254    CycleDetected { nodes: Vec<String> },
255    /// An edge label was too long to render inline and was moved to a legend
256    LabelDropped {
257        marker: String,
258        edge_from: String,
259        edge_to: String,
260        label: String,
261    },
262    /// A D2 feature is not supported in TUI rendering
263    UnsupportedFeature { feature: String, line: usize },
264}
265
266impl fmt::Display for DiagramWarning {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        match self {
269            DiagramWarning::CycleDetected { nodes } => {
270                write!(f, "Cycle detected involving nodes: {}", nodes.join(", "))
271            }
272            DiagramWarning::LabelDropped {
273                marker,
274                edge_from,
275                edge_to,
276                label,
277            } => {
278                write!(
279                    f,
280                    "Label '{}' on edge {} -> {} moved to legend as {}",
281                    label, edge_from, edge_to, marker
282                )
283            }
284            DiagramWarning::UnsupportedFeature { feature, line } => {
285                write!(f, "Unsupported D2 feature '{}' on line {}", feature, line)
286            }
287        }
288    }
289}
290
291/// Result of rendering a diagram
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct RenderResult {
294    /// The rendered diagram output
295    pub output: String,
296    /// Warnings generated during layout/rendering
297    pub warnings: Vec<DiagramWarning>,
298}