pub mod ansi;
mod render;
pub use ansi::{AnsiSegment, parse_ansi};
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::scroll::ScrollState;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TerminalOutputMessage {
PushLine(String),
PushLines(Vec<String>),
Clear,
ScrollUp,
ScrollDown,
PageUp(usize),
PageDown(usize),
Home,
End,
ToggleAutoScroll,
ToggleLineNumbers,
SetRunning(bool),
SetExitCode(Option<i32>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TerminalOutputOutput {
ScrollChanged(usize),
LineAdded(usize),
Cleared,
AutoScrollToggled(bool),
LineNumbersToggled(bool),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TerminalOutputState {
lines: Vec<String>,
max_lines: usize,
scroll: ScrollState,
auto_scroll: bool,
show_line_numbers: bool,
title: Option<String>,
exit_code: Option<i32>,
running: bool,
}
impl Default for TerminalOutputState {
fn default() -> Self {
Self {
lines: Vec::new(),
max_lines: 10_000,
scroll: ScrollState::new(0),
auto_scroll: true,
show_line_numbers: false,
title: None,
exit_code: None,
running: false,
}
}
}
impl TerminalOutputState {
pub fn new() -> Self {
Self::default()
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_max_lines(mut self, max: usize) -> Self {
self.max_lines = max;
self
}
pub fn with_auto_scroll(mut self, auto_scroll: bool) -> Self {
self.auto_scroll = auto_scroll;
self
}
pub fn with_line_numbers(mut self, show: bool) -> Self {
self.show_line_numbers = show;
self
}
pub fn with_running(mut self, running: bool) -> Self {
self.running = running;
self
}
pub fn push_line(&mut self, line: impl Into<String>) {
self.lines.push(line.into());
self.enforce_max_lines();
self.scroll.set_content_length(self.lines.len());
if self.auto_scroll {
self.scroll.scroll_to_end();
}
}
pub fn push_lines(&mut self, lines: Vec<String>) {
self.lines.extend(lines);
self.enforce_max_lines();
self.scroll.set_content_length(self.lines.len());
if self.auto_scroll {
self.scroll.scroll_to_end();
}
}
pub fn clear(&mut self) {
self.lines.clear();
self.scroll = ScrollState::new(0);
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn scroll_offset(&self) -> usize {
self.scroll.offset()
}
pub fn set_scroll_offset(&mut self, offset: usize) {
self.scroll.set_offset(offset);
}
pub fn max_lines(&self) -> usize {
self.max_lines
}
pub fn set_max_lines(&mut self, max: usize) {
self.max_lines = max;
self.enforce_max_lines();
}
pub fn auto_scroll(&self) -> bool {
self.auto_scroll
}
pub fn set_auto_scroll(&mut self, auto_scroll: bool) {
self.auto_scroll = auto_scroll;
}
pub fn show_line_numbers(&self) -> bool {
self.show_line_numbers
}
pub fn set_show_line_numbers(&mut self, show: bool) {
self.show_line_numbers = show;
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn exit_code(&self) -> Option<i32> {
self.exit_code
}
pub fn set_exit_code(&mut self, code: Option<i32>) {
self.exit_code = code;
if code.is_some() {
self.running = false;
}
}
pub fn running(&self) -> bool {
self.running
}
pub fn set_running(&mut self, running: bool) {
self.running = running;
}
pub fn update(&mut self, msg: TerminalOutputMessage) -> Option<TerminalOutputOutput> {
TerminalOutput::update(self, msg)
}
fn enforce_max_lines(&mut self) {
if self.lines.len() > self.max_lines {
let excess = self.lines.len() - self.max_lines;
self.lines.drain(..excess);
self.scroll.set_content_length(self.lines.len());
}
}
}
pub struct TerminalOutput;
impl Component for TerminalOutput {
type State = TerminalOutputState;
type Message = TerminalOutputMessage;
type Output = TerminalOutputOutput;
fn init() -> Self::State {
TerminalOutputState::default()
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
let key = event.as_key()?;
let ctrl = key.modifiers.ctrl();
match key.code {
Key::Up | Key::Char('k') if !ctrl => Some(TerminalOutputMessage::ScrollUp),
Key::Down | Key::Char('j') if !ctrl => Some(TerminalOutputMessage::ScrollDown),
Key::PageUp => Some(TerminalOutputMessage::PageUp(10)),
Key::PageDown => Some(TerminalOutputMessage::PageDown(10)),
Key::Char('u') if ctrl => Some(TerminalOutputMessage::PageUp(10)),
Key::Char('d') if ctrl => Some(TerminalOutputMessage::PageDown(10)),
Key::Char('g') if key.modifiers.shift() => Some(TerminalOutputMessage::End),
Key::Home | Key::Char('g') => Some(TerminalOutputMessage::Home),
Key::End => Some(TerminalOutputMessage::End),
Key::Char('a') if !ctrl => Some(TerminalOutputMessage::ToggleAutoScroll),
Key::Char('n') if !ctrl => Some(TerminalOutputMessage::ToggleLineNumbers),
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
TerminalOutputMessage::PushLine(line) => {
state.push_line(line);
Some(TerminalOutputOutput::LineAdded(state.lines.len()))
}
TerminalOutputMessage::PushLines(lines) => {
let count = lines.len();
state.push_lines(lines);
if count > 0 {
Some(TerminalOutputOutput::LineAdded(state.lines.len()))
} else {
None
}
}
TerminalOutputMessage::Clear => {
if state.lines.is_empty() {
None
} else {
state.clear();
Some(TerminalOutputOutput::Cleared)
}
}
TerminalOutputMessage::ScrollUp => {
if state.auto_scroll {
state.auto_scroll = false;
}
if state.scroll.scroll_up() {
Some(TerminalOutputOutput::ScrollChanged(state.scroll.offset()))
} else {
None
}
}
TerminalOutputMessage::ScrollDown => {
if state.scroll.scroll_down() {
Some(TerminalOutputOutput::ScrollChanged(state.scroll.offset()))
} else {
None
}
}
TerminalOutputMessage::PageUp(n) => {
if state.auto_scroll {
state.auto_scroll = false;
}
if state.scroll.page_up(n) {
Some(TerminalOutputOutput::ScrollChanged(state.scroll.offset()))
} else {
None
}
}
TerminalOutputMessage::PageDown(n) => {
if state.scroll.page_down(n) {
Some(TerminalOutputOutput::ScrollChanged(state.scroll.offset()))
} else {
None
}
}
TerminalOutputMessage::Home => {
if state.auto_scroll {
state.auto_scroll = false;
}
if state.scroll.scroll_to_start() {
Some(TerminalOutputOutput::ScrollChanged(0))
} else {
None
}
}
TerminalOutputMessage::End => {
if state.scroll.scroll_to_end() {
Some(TerminalOutputOutput::ScrollChanged(state.scroll.offset()))
} else {
None
}
}
TerminalOutputMessage::ToggleAutoScroll => {
state.auto_scroll = !state.auto_scroll;
if state.auto_scroll {
state.scroll.set_content_length(state.lines.len());
state.scroll.scroll_to_end();
}
Some(TerminalOutputOutput::AutoScrollToggled(state.auto_scroll))
}
TerminalOutputMessage::ToggleLineNumbers => {
state.show_line_numbers = !state.show_line_numbers;
Some(TerminalOutputOutput::LineNumbersToggled(
state.show_line_numbers,
))
}
TerminalOutputMessage::SetRunning(running) => {
state.running = running;
None
}
TerminalOutputMessage::SetExitCode(code) => {
state.exit_code = code;
if code.is_some() {
state.running = false;
}
None
}
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod tests;