Move chat code to chat.rs
This commit is contained in:
parent
65d23692fb
commit
f29b4be09c
2 changed files with 226 additions and 228 deletions
248
src/user/chat.rs
248
src/user/chat.rs
|
|
@ -13,11 +13,20 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ActivePane, App, HotkeyAction, Marker, PaneState, ScreenAction, ScreenView,
|
App, HotkeyAction, ScreenAction, ScreenView,
|
||||||
new_textarea, screen_legend,
|
screen_legend,
|
||||||
};
|
};
|
||||||
use crate::user::ui_channel::{UiMessage, StreamTarget};
|
use crate::user::ui_channel::{UiMessage, StreamTarget};
|
||||||
|
|
||||||
|
/// Turn marker for the conversation pane gutter.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Default)]
|
||||||
|
enum Marker {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
User,
|
||||||
|
Assistant,
|
||||||
|
}
|
||||||
|
|
||||||
enum PaneTarget {
|
enum PaneTarget {
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationAssistant,
|
ConversationAssistant,
|
||||||
|
|
@ -25,28 +34,223 @@ enum PaneTarget {
|
||||||
ToolResult,
|
ToolResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_PANE_LINES: usize = 10_000;
|
||||||
|
|
||||||
|
/// Which pane receives scroll keys.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum ActivePane {
|
||||||
|
Autonomous,
|
||||||
|
Conversation,
|
||||||
|
Tools,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_ansi(text: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(text.len());
|
||||||
|
let mut chars = text.chars().peekable();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch == '\x1b' {
|
||||||
|
if chars.peek() == Some(&'[') {
|
||||||
|
chars.next();
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if c.is_ascii() && (0x20..=0x3F).contains(&(c as u8)) {
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(&c) = chars.peek() {
|
||||||
|
if c.is_ascii() && (0x40..=0x7E).contains(&(c as u8)) {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(&c) = chars.peek() {
|
||||||
|
if c.is_ascii() && (0x40..=0x5F).contains(&(c as u8)) {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_zero_width(ch: char) -> bool {
|
||||||
|
matches!(ch,
|
||||||
|
'\u{200B}'..='\u{200F}' |
|
||||||
|
'\u{2028}'..='\u{202F}' |
|
||||||
|
'\u{2060}'..='\u{2069}' |
|
||||||
|
'\u{FEFF}'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_textarea(lines: Vec<String>) -> tui_textarea::TextArea<'static> {
|
||||||
|
let mut ta = tui_textarea::TextArea::new(lines);
|
||||||
|
ta.set_cursor_line_style(Style::default());
|
||||||
|
ta.set_wrap_mode(tui_textarea::WrapMode::Word);
|
||||||
|
ta
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_markdown(md: &str) -> Vec<Line<'static>> {
|
||||||
|
tui_markdown::from_str(md)
|
||||||
|
.lines
|
||||||
|
.into_iter()
|
||||||
|
.map(|line| {
|
||||||
|
let spans: Vec<Span<'static>> = line.spans.into_iter()
|
||||||
|
.map(|span| Span::styled(span.content.into_owned(), span.style))
|
||||||
|
.collect();
|
||||||
|
let mut result = Line::from(spans).style(line.style);
|
||||||
|
result.alignment = line.alignment;
|
||||||
|
result
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PaneState {
|
||||||
|
lines: Vec<Line<'static>>,
|
||||||
|
markers: Vec<Marker>,
|
||||||
|
current_line: String,
|
||||||
|
current_color: Color,
|
||||||
|
md_buffer: String,
|
||||||
|
use_markdown: bool,
|
||||||
|
pending_marker: Marker,
|
||||||
|
scroll: u16,
|
||||||
|
pinned: bool,
|
||||||
|
last_total_lines: u16,
|
||||||
|
last_height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaneState {
|
||||||
|
fn new(use_markdown: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
lines: Vec::new(), markers: Vec::new(),
|
||||||
|
current_line: String::new(), current_color: Color::Reset,
|
||||||
|
md_buffer: String::new(), use_markdown,
|
||||||
|
pending_marker: Marker::None, scroll: 0, pinned: false,
|
||||||
|
last_total_lines: 0, last_height: 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evict(&mut self) {
|
||||||
|
if self.lines.len() > MAX_PANE_LINES {
|
||||||
|
let excess = self.lines.len() - MAX_PANE_LINES;
|
||||||
|
self.lines.drain(..excess);
|
||||||
|
self.markers.drain(..excess);
|
||||||
|
self.scroll = self.scroll.saturating_sub(excess as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_text(&mut self, text: &str) {
|
||||||
|
let clean = strip_ansi(text);
|
||||||
|
if self.use_markdown {
|
||||||
|
self.md_buffer.push_str(&clean);
|
||||||
|
} else {
|
||||||
|
for ch in clean.chars() {
|
||||||
|
if ch == '\n' {
|
||||||
|
let line = std::mem::take(&mut self.current_line);
|
||||||
|
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
|
||||||
|
self.markers.push(Marker::None);
|
||||||
|
} else if ch == '\t' {
|
||||||
|
self.current_line.push_str(" ");
|
||||||
|
} else if ch.is_control() || is_zero_width(ch) {
|
||||||
|
} else {
|
||||||
|
self.current_line.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.evict();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_pending(&mut self) {
|
||||||
|
if self.use_markdown && !self.md_buffer.is_empty() {
|
||||||
|
let parsed = parse_markdown(&self.md_buffer);
|
||||||
|
for (i, line) in parsed.into_iter().enumerate() {
|
||||||
|
let marker = if i == 0 { std::mem::take(&mut self.pending_marker) } else { Marker::None };
|
||||||
|
self.lines.push(line);
|
||||||
|
self.markers.push(marker);
|
||||||
|
}
|
||||||
|
self.md_buffer.clear();
|
||||||
|
}
|
||||||
|
if !self.current_line.is_empty() {
|
||||||
|
let line = std::mem::take(&mut self.current_line);
|
||||||
|
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
|
||||||
|
self.markers.push(std::mem::take(&mut self.pending_marker));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_line(&mut self, line: String, color: Color) {
|
||||||
|
self.push_line_with_marker(line, color, Marker::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_line_with_marker(&mut self, line: String, color: Color, marker: Marker) {
|
||||||
|
self.flush_pending();
|
||||||
|
self.lines.push(Line::styled(strip_ansi(&line), Style::default().fg(color)));
|
||||||
|
self.markers.push(marker);
|
||||||
|
self.evict();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_line(&mut self) {
|
||||||
|
self.lines.pop();
|
||||||
|
self.markers.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_up(&mut self, n: u16) {
|
||||||
|
self.scroll = self.scroll.saturating_sub(n);
|
||||||
|
self.pinned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_down(&mut self, n: u16) {
|
||||||
|
let max = self.last_total_lines.saturating_sub(self.last_height);
|
||||||
|
self.scroll = (self.scroll + n).min(max);
|
||||||
|
if self.scroll >= max { self.pinned = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_lines(&self) -> Vec<Line<'static>> {
|
||||||
|
let (lines, _) = self.all_lines_with_markers();
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_lines_with_markers(&self) -> (Vec<Line<'static>>, Vec<Marker>) {
|
||||||
|
let mut lines: Vec<Line<'static>> = self.lines.clone();
|
||||||
|
let mut markers: Vec<Marker> = self.markers.clone();
|
||||||
|
if self.use_markdown && !self.md_buffer.is_empty() {
|
||||||
|
let parsed = parse_markdown(&self.md_buffer);
|
||||||
|
let count = parsed.len();
|
||||||
|
lines.extend(parsed);
|
||||||
|
if count > 0 {
|
||||||
|
markers.push(self.pending_marker);
|
||||||
|
markers.extend(std::iter::repeat(Marker::None).take(count - 1));
|
||||||
|
}
|
||||||
|
} else if !self.current_line.is_empty() {
|
||||||
|
lines.push(Line::styled(self.current_line.clone(), Style::default().fg(self.current_color)));
|
||||||
|
markers.push(self.pending_marker);
|
||||||
|
}
|
||||||
|
(lines, markers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct InteractScreen {
|
pub(crate) struct InteractScreen {
|
||||||
pub(crate) autonomous: PaneState,
|
autonomous: PaneState,
|
||||||
pub(crate) conversation: PaneState,
|
conversation: PaneState,
|
||||||
pub(crate) tools: PaneState,
|
tools: PaneState,
|
||||||
pub(crate) textarea: tui_textarea::TextArea<'static>,
|
textarea: tui_textarea::TextArea<'static>,
|
||||||
pub(crate) input_history: Vec<String>,
|
input_history: Vec<String>,
|
||||||
pub(crate) history_index: Option<usize>,
|
history_index: Option<usize>,
|
||||||
pub(crate) active_pane: ActivePane,
|
active_pane: ActivePane,
|
||||||
pub(crate) pane_areas: [Rect; 3],
|
pane_areas: [Rect; 3],
|
||||||
pub(crate) needs_assistant_marker: bool,
|
needs_assistant_marker: bool,
|
||||||
pub(crate) turn_started: Option<std::time::Instant>,
|
turn_started: Option<std::time::Instant>,
|
||||||
pub(crate) call_started: Option<std::time::Instant>,
|
call_started: Option<std::time::Instant>,
|
||||||
pub(crate) call_timeout_secs: u64,
|
call_timeout_secs: u64,
|
||||||
// State sync with agent — double buffer
|
// State sync with agent — double buffer
|
||||||
last_generation: u64,
|
last_generation: u64,
|
||||||
last_entries: Vec<crate::agent::context::ConversationEntry>,
|
last_entries: Vec<crate::agent::context::ConversationEntry>,
|
||||||
/// Reference to agent for state sync
|
/// Reference to agent for state sync
|
||||||
agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>,
|
agent: std::sync::Arc<std::sync::Mutex<crate::agent::Agent>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InteractScreen {
|
impl InteractScreen {
|
||||||
pub fn new(agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>) -> Self {
|
pub fn new(agent: std::sync::Arc<std::sync::Mutex<crate::agent::Agent>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
autonomous: PaneState::new(true),
|
autonomous: PaneState::new(true),
|
||||||
conversation: PaneState::new(true),
|
conversation: PaneState::new(true),
|
||||||
|
|
@ -115,12 +319,12 @@ impl InteractScreen {
|
||||||
|
|
||||||
/// Sync conversation display from agent entries.
|
/// Sync conversation display from agent entries.
|
||||||
fn sync_from_agent(&mut self) {
|
fn sync_from_agent(&mut self) {
|
||||||
let agent = self.agent.blocking_lock();
|
let agent = self.agent.lock().unwrap();
|
||||||
let gen = agent.generation;
|
let generation = agent.generation;
|
||||||
let entries = agent.entries();
|
let entries = agent.entries();
|
||||||
|
|
||||||
// Phase 1: detect desync and pop
|
// Phase 1: detect desync and pop
|
||||||
if gen != self.last_generation {
|
if generation != self.last_generation {
|
||||||
self.conversation = PaneState::new(true);
|
self.conversation = PaneState::new(true);
|
||||||
self.autonomous = PaneState::new(true);
|
self.autonomous = PaneState::new(true);
|
||||||
self.tools = PaneState::new(false);
|
self.tools = PaneState::new(false);
|
||||||
|
|
@ -165,7 +369,7 @@ impl InteractScreen {
|
||||||
self.last_entries.push(entry.clone());
|
self.last_entries.push(entry.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_generation = gen;
|
self.last_generation = generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a UiMessage — update pane state.
|
/// Process a UiMessage — update pane state.
|
||||||
|
|
@ -260,7 +464,7 @@ impl InteractScreen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_mouse(&mut self, mouse: MouseEvent) {
|
fn handle_mouse(&mut self, mouse: MouseEvent) {
|
||||||
match mouse.kind {
|
match mouse.kind {
|
||||||
MouseEventKind::ScrollUp => self.scroll_active_up(3),
|
MouseEventKind::ScrollUp => self.scroll_active_up(3),
|
||||||
MouseEventKind::ScrollDown => self.scroll_active_down(3),
|
MouseEventKind::ScrollDown => self.scroll_active_down(3),
|
||||||
|
|
@ -278,7 +482,7 @@ impl InteractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the main (F1) screen — four-pane layout with status bar.
|
/// Draw the main (F1) screen — four-pane layout with status bar.
|
||||||
pub(crate) fn draw_main(&mut self, frame: &mut Frame, size: Rect, app: &App) {
|
fn draw_main(&mut self, frame: &mut Frame, size: Rect, app: &App) {
|
||||||
// Main layout: content area + active tools overlay + status bar
|
// Main layout: content area + active tools overlay + status bar
|
||||||
let active_tools = app.active_tools.lock().unwrap();
|
let active_tools = app.active_tools.lock().unwrap();
|
||||||
let tool_lines = active_tools.len() as u16;
|
let tool_lines = active_tools.len() as u16;
|
||||||
|
|
|
||||||
206
src/user/mod.rs
206
src/user/mod.rs
|
|
@ -21,8 +21,6 @@ use ratatui::crossterm::{
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
style::{Color, Style},
|
|
||||||
text::{Line, Span},
|
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
|
@ -48,210 +46,6 @@ pub(crate) fn screen_legend() -> String {
|
||||||
SCREEN_LEGEND.get().cloned().unwrap_or_default()
|
SCREEN_LEGEND.get().cloned().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn strip_ansi(text: &str) -> String {
|
|
||||||
let mut out = String::with_capacity(text.len());
|
|
||||||
let mut chars = text.chars().peekable();
|
|
||||||
while let Some(ch) = chars.next() {
|
|
||||||
if ch == '\x1b' {
|
|
||||||
if chars.peek() == Some(&'[') {
|
|
||||||
chars.next();
|
|
||||||
while let Some(&c) = chars.peek() {
|
|
||||||
if c.is_ascii() && (0x20..=0x3F).contains(&(c as u8)) {
|
|
||||||
chars.next();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(&c) = chars.peek() {
|
|
||||||
if c.is_ascii() && (0x40..=0x7E).contains(&(c as u8)) {
|
|
||||||
chars.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some(&c) = chars.peek() {
|
|
||||||
if c.is_ascii() && (0x40..=0x5F).contains(&(c as u8)) {
|
|
||||||
chars.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_zero_width(ch: char) -> bool {
|
|
||||||
matches!(ch,
|
|
||||||
'\u{200B}'..='\u{200F}' |
|
|
||||||
'\u{2028}'..='\u{202F}' |
|
|
||||||
'\u{2060}'..='\u{2069}' |
|
|
||||||
'\u{FEFF}'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Which pane receives scroll keys.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum ActivePane {
|
|
||||||
Autonomous,
|
|
||||||
Conversation,
|
|
||||||
Tools,
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_PANE_LINES: usize = 10_000;
|
|
||||||
|
|
||||||
/// Turn marker for the conversation pane gutter.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Default)]
|
|
||||||
pub(crate) enum Marker {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
User,
|
|
||||||
Assistant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct PaneState {
|
|
||||||
pub(crate) lines: Vec<Line<'static>>,
|
|
||||||
pub(crate) markers: Vec<Marker>,
|
|
||||||
pub(crate) current_line: String,
|
|
||||||
pub(crate) current_color: Color,
|
|
||||||
pub(crate) md_buffer: String,
|
|
||||||
pub(crate) use_markdown: bool,
|
|
||||||
pub(crate) pending_marker: Marker,
|
|
||||||
pub(crate) scroll: u16,
|
|
||||||
pub(crate) pinned: bool,
|
|
||||||
pub(crate) last_total_lines: u16,
|
|
||||||
pub(crate) last_height: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaneState {
|
|
||||||
fn new(use_markdown: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
lines: Vec::new(), markers: Vec::new(),
|
|
||||||
current_line: String::new(), current_color: Color::Reset,
|
|
||||||
md_buffer: String::new(), use_markdown,
|
|
||||||
pending_marker: Marker::None, scroll: 0, pinned: false,
|
|
||||||
last_total_lines: 0, last_height: 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn evict(&mut self) {
|
|
||||||
if self.lines.len() > MAX_PANE_LINES {
|
|
||||||
let excess = self.lines.len() - MAX_PANE_LINES;
|
|
||||||
self.lines.drain(..excess);
|
|
||||||
self.markers.drain(..excess);
|
|
||||||
self.scroll = self.scroll.saturating_sub(excess as u16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_text(&mut self, text: &str) {
|
|
||||||
let clean = strip_ansi(text);
|
|
||||||
if self.use_markdown {
|
|
||||||
self.md_buffer.push_str(&clean);
|
|
||||||
} else {
|
|
||||||
for ch in clean.chars() {
|
|
||||||
if ch == '\n' {
|
|
||||||
let line = std::mem::take(&mut self.current_line);
|
|
||||||
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
|
|
||||||
self.markers.push(Marker::None);
|
|
||||||
} else if ch == '\t' {
|
|
||||||
self.current_line.push_str(" ");
|
|
||||||
} else if ch.is_control() || is_zero_width(ch) {
|
|
||||||
} else {
|
|
||||||
self.current_line.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.evict();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn flush_pending(&mut self) {
|
|
||||||
if self.use_markdown && !self.md_buffer.is_empty() {
|
|
||||||
let parsed = parse_markdown(&self.md_buffer);
|
|
||||||
for (i, line) in parsed.into_iter().enumerate() {
|
|
||||||
let marker = if i == 0 { std::mem::take(&mut self.pending_marker) } else { Marker::None };
|
|
||||||
self.lines.push(line);
|
|
||||||
self.markers.push(marker);
|
|
||||||
}
|
|
||||||
self.md_buffer.clear();
|
|
||||||
}
|
|
||||||
if !self.current_line.is_empty() {
|
|
||||||
let line = std::mem::take(&mut self.current_line);
|
|
||||||
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
|
|
||||||
self.markers.push(std::mem::take(&mut self.pending_marker));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_line(&mut self, line: String, color: Color) {
|
|
||||||
self.push_line_with_marker(line, color, Marker::None);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_line_with_marker(&mut self, line: String, color: Color, marker: Marker) {
|
|
||||||
self.flush_pending();
|
|
||||||
self.lines.push(Line::styled(strip_ansi(&line), Style::default().fg(color)));
|
|
||||||
self.markers.push(marker);
|
|
||||||
self.evict();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn pop_line(&mut self) {
|
|
||||||
self.lines.pop();
|
|
||||||
self.markers.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_up(&mut self, n: u16) {
|
|
||||||
self.scroll = self.scroll.saturating_sub(n);
|
|
||||||
self.pinned = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_down(&mut self, n: u16) {
|
|
||||||
let max = self.last_total_lines.saturating_sub(self.last_height);
|
|
||||||
self.scroll = (self.scroll + n).min(max);
|
|
||||||
if self.scroll >= max { self.pinned = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn all_lines(&self) -> Vec<Line<'static>> {
|
|
||||||
let (lines, _) = self.all_lines_with_markers();
|
|
||||||
lines
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn all_lines_with_markers(&self) -> (Vec<Line<'static>>, Vec<Marker>) {
|
|
||||||
let mut lines: Vec<Line<'static>> = self.lines.clone();
|
|
||||||
let mut markers: Vec<Marker> = self.markers.clone();
|
|
||||||
if self.use_markdown && !self.md_buffer.is_empty() {
|
|
||||||
let parsed = parse_markdown(&self.md_buffer);
|
|
||||||
let count = parsed.len();
|
|
||||||
lines.extend(parsed);
|
|
||||||
if count > 0 {
|
|
||||||
markers.push(self.pending_marker);
|
|
||||||
markers.extend(std::iter::repeat(Marker::None).take(count - 1));
|
|
||||||
}
|
|
||||||
} else if !self.current_line.is_empty() {
|
|
||||||
lines.push(Line::styled(self.current_line.clone(), Style::default().fg(self.current_color)));
|
|
||||||
markers.push(self.pending_marker);
|
|
||||||
}
|
|
||||||
(lines, markers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new_textarea(lines: Vec<String>) -> tui_textarea::TextArea<'static> {
|
|
||||||
let mut ta = tui_textarea::TextArea::new(lines);
|
|
||||||
ta.set_cursor_line_style(Style::default());
|
|
||||||
ta.set_wrap_mode(tui_textarea::WrapMode::Word);
|
|
||||||
ta
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
|
|
||||||
tui_markdown::from_str(md)
|
|
||||||
.lines
|
|
||||||
.into_iter()
|
|
||||||
.map(|line| {
|
|
||||||
let spans: Vec<Span<'static>> = line.spans.into_iter()
|
|
||||||
.map(|span| Span::styled(span.content.into_owned(), span.style))
|
|
||||||
.collect();
|
|
||||||
let mut result = Line::from(spans).style(line.style);
|
|
||||||
result.alignment = line.alignment;
|
|
||||||
result
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Action returned from a screen's tick method.
|
/// Action returned from a screen's tick method.
|
||||||
pub enum ScreenAction {
|
pub enum ScreenAction {
|
||||||
/// Switch to screen at this index
|
/// Switch to screen at this index
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue