zeph_tui/types.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Metadata for an active paste in the input buffer.
5///
6/// Present only while the input contains unsubmitted pasted text that was
7/// multiline (two or more lines). Single-line pastes do not set this.
8///
9/// # Examples
10///
11/// ```rust
12/// use zeph_tui::PasteState;
13///
14/// let ps = PasteState { line_count: 5, byte_len: 128 };
15/// assert_eq!(ps.line_count, 5);
16/// ```
17#[derive(Debug, Clone)]
18pub struct PasteState {
19 /// Number of lines in the pasted text (always >= 2).
20 pub line_count: usize,
21 /// Byte length of the pasted text.
22 pub byte_len: usize,
23}
24
25/// The current text-input mode of the TUI.
26///
27/// Inspired by modal editors: in `Normal` mode key bindings trigger actions;
28/// in `Insert` mode printable characters are appended to the input buffer.
29///
30/// # Examples
31///
32/// ```rust
33/// use zeph_tui::InputMode;
34///
35/// let mode = InputMode::Insert;
36/// assert_eq!(mode, InputMode::Insert);
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum InputMode {
40 /// Navigation and command keybindings are active; typing does not insert text.
41 Normal,
42 /// Text is inserted into the input field on every printable key press.
43 Insert,
44}
45
46/// The role of a message displayed in the chat widget.
47///
48/// The role controls the display style (colour, prefix label) applied by the
49/// chat renderer.
50///
51/// # Examples
52///
53/// ```rust
54/// use zeph_tui::MessageRole;
55///
56/// let role = MessageRole::User;
57/// assert_eq!(role, MessageRole::User);
58/// ```
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum MessageRole {
61 /// A message sent by the human user.
62 User,
63 /// A message generated by the AI assistant.
64 Assistant,
65 /// An internal system or meta message (e.g. session start notice).
66 System,
67 /// Output from a tool call execution.
68 Tool,
69}
70
71/// A single entry in the TUI chat history buffer.
72///
73/// Carries the rendered text, role metadata, optional tool context, an inline
74/// diff, and a wall-clock timestamp for display.
75///
76/// # Examples
77///
78/// ```rust
79/// use zeph_tui::{ChatMessage, MessageRole};
80///
81/// let msg = ChatMessage::new(MessageRole::User, "Hello, agent!");
82/// assert_eq!(msg.role, MessageRole::User);
83/// assert_eq!(msg.content, "Hello, agent!");
84/// assert!(!msg.streaming);
85/// ```
86#[derive(Debug, Clone)]
87pub struct ChatMessage {
88 /// Role that determines rendering style.
89 pub role: MessageRole,
90 /// Rendered text content of the message.
91 pub content: String,
92 /// `true` while the message is still being streamed from the LLM.
93 pub streaming: bool,
94 /// Name of the tool that produced this message, if any.
95 pub tool_name: Option<zeph_common::ToolName>,
96 /// Inline diff attached to a tool-output message.
97 pub diff_data: Option<zeph_core::DiffData>,
98 /// Human-readable filter statistics (e.g. "kept 12/40 lines").
99 pub filter_stats: Option<String>,
100 /// 0-based line indices preserved by the output filter, used for
101 /// highlighting in the diff widget.
102 pub kept_lines: Option<Vec<usize>>,
103 /// Wall-clock time formatted as `HH:MM` when the message was created.
104 pub timestamp: String,
105 /// Number of lines in the pasted content when this message was submitted
106 /// from a paste. `Some(n)` (n >= 2) enables collapsible display in the
107 /// chat renderer; `None` means normal display.
108 pub paste_line_count: Option<usize>,
109}
110
111impl ChatMessage {
112 /// Create a new non-streaming message with the current local time as timestamp.
113 ///
114 /// # Examples
115 ///
116 /// ```rust
117 /// use zeph_tui::{ChatMessage, MessageRole};
118 ///
119 /// let msg = ChatMessage::new(MessageRole::Assistant, "Done.");
120 /// assert_eq!(msg.role, MessageRole::Assistant);
121 /// assert!(!msg.streaming);
122 /// ```
123 pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
124 Self {
125 role,
126 content: content.into(),
127 streaming: false,
128 tool_name: None,
129 diff_data: None,
130 filter_stats: None,
131 kept_lines: None,
132 timestamp: format_local_time(),
133 paste_line_count: None,
134 }
135 }
136
137 /// Mark this message as actively streaming.
138 ///
139 /// The chat widget renders a blinking cursor after the content while
140 /// `streaming` is `true`.
141 ///
142 /// # Examples
143 ///
144 /// ```rust
145 /// use zeph_tui::{ChatMessage, MessageRole};
146 ///
147 /// let msg = ChatMessage::new(MessageRole::Assistant, "").streaming();
148 /// assert!(msg.streaming);
149 /// ```
150 #[must_use]
151 pub fn streaming(mut self) -> Self {
152 self.streaming = true;
153 self
154 }
155
156 /// Attach a tool name to this message for display in the chat header.
157 ///
158 /// # Examples
159 ///
160 /// ```rust
161 /// use zeph_tui::{ChatMessage, MessageRole};
162 ///
163 /// let msg = ChatMessage::new(MessageRole::Tool, "output")
164 /// .with_tool(zeph_common::ToolName::new("bash"));
165 /// assert!(msg.tool_name.is_some());
166 /// ```
167 #[must_use]
168 pub fn with_tool(mut self, name: zeph_common::ToolName) -> Self {
169 self.tool_name = Some(name);
170 self
171 }
172}
173
174fn format_local_time() -> String {
175 chrono::Local::now().format("%H:%M").to_string()
176}