Files
yggdrasil/userspace/tools/red/src/state.rs
T
2026-06-04 17:35:55 +03:00

248 lines
7.2 KiB
Rust

use std::{cell::RefCell, fmt::Write as FmtWrite, path::Path, rc::Rc};
use libterm::{Clear, Color, Term, TermKey};
use lysp::vm::value::convert::AnyFunction;
use crate::{
TopMode,
buffer::{Buffer, Mode, SetMode},
config::EditorConfig,
error::Error,
highlight::Highlighter,
script::Movement,
};
pub struct State {
pub(super) term: Term,
buffer: Buffer,
top_mode: TopMode,
config: Rc<RefCell<EditorConfig>>,
running: bool,
number_width: usize,
pub(super) highlight: Highlighter,
// Scripting hooks
pub(super) key_hook: Option<AnyFunction>,
pub(super) post_render_hook: Option<AnyFunction>,
}
impl State {
pub fn open<P: AsRef<Path>>(
path: Option<P>,
config: Rc<RefCell<EditorConfig>>,
) -> Result<Self, Error> {
let mut buffer = match path {
Some(path) => Buffer::open(path).unwrap(),
None => Buffer::empty(),
};
let term = Term::open().map_err(Error::TerminalError)?;
Ok(Self {
number_width: buffer.number_width(),
top_mode: TopMode::Normal,
running: true,
buffer,
term,
config,
highlight: Highlighter::default(),
key_hook: None,
post_render_hook: None,
})
}
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
pub fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffer
}
pub fn mode(&self) -> (TopMode, Mode) {
(self.top_mode, self.buffer.mode())
}
pub fn buffer_terminal_cursor(&self) -> Option<(usize, usize)> {
if self.top_mode == TopMode::Command {
return None;
}
Some(self.buffer.get_terminal_cursor(&self.config.borrow()))
}
pub fn exit(&mut self, force: bool) -> Result<(), Error> {
if self.buffer.is_modified() && !force {
Err(Error::UnsavedBuffer("Use :q! to force-exit"))
} else {
self.running = false;
Ok(())
}
}
pub fn exited(&self) -> bool {
!self.running
}
fn display_number(&mut self) -> Result<(), Error> {
let start = self.buffer.row_offset();
let end = self.buffer.len();
for i in 0.. {
self.term
.set_cursor_position(i, 0)
.map_err(Error::TerminalError)?;
if i + start == self.buffer.cursor_row() {
self.term.set_bright(true).map_err(Error::TerminalError)?;
self.term
.set_foreground(Color::Yellow)
.map_err(Error::TerminalError)?;
}
if i + start < end {
write!(self.term, " {0:1$} ", i + start + 1, self.number_width)
.map_err(Error::TerminalFmtError)?;
}
if i == self.buffer.height() {
break;
}
if i + start == self.buffer.cursor_row() {
self.term.reset_style().map_err(Error::TerminalError)?;
}
}
self.term.reset_style().map_err(Error::TerminalError)?;
Ok(())
}
pub fn cursor_to_buffer(&mut self) -> Result<(), Error> {
let config = self.config.borrow();
self.buffer.set_terminal_cursor(&config, &mut self.term)
}
pub fn finish_display(&mut self) -> Result<(), Error> {
self.term.flush().map_err(Error::TerminalError)?;
Ok(())
}
pub fn display(&mut self) -> Result<(usize, usize), Error> {
let config = self.config.clone();
let mut config = config.borrow_mut();
if self.buffer.is_dirty() {
self.term.clear(Clear::All).map_err(Error::TerminalError)?;
}
let (w, h) = self.term.size().map_err(Error::TerminalError)?;
if config.number.clean() || config.bottom_margin.clean() {
if *config.number {
let nw = self.buffer.number_width() + 3;
self.buffer
.resize(&config, nw, w - nw - 1, h - (*config.bottom_margin + 1));
} else {
self.buffer
.resize(&config, 0, w - 1, h - (*config.bottom_margin + 1));
}
}
if *config.number && self.buffer.is_dirty() {
self.display_number()?;
}
self.buffer
.display(&config, &mut self.term, &self.highlight)?;
Ok((w, h))
}
pub fn set_mode(&mut self, target: SetMode) {
self.top_mode = TopMode::Normal;
self.buffer.set_mode(&self.config.borrow(), target);
}
pub fn set_top_mode(&mut self, target: TopMode) {
if self.top_mode == target {
return;
}
self.top_mode = target;
self.buffer.set_mode(&self.config.borrow(), SetMode::Normal);
}
pub fn write_text(&mut self, text: &str) {
let config = self.config.borrow();
for char in text.chars() {
self.buffer.insert(&config, char);
}
}
pub fn move_cursor(&mut self, movement: Movement) {
let config = self.config.borrow();
match movement {
Movement::LineStart => self.buffer.set_column(&config, 0),
Movement::LineEnd => self.buffer.to_line_end(&config),
Movement::NextLine => self.buffer.move_cursor(&config, 0, 1),
Movement::PrevLine => self.buffer.move_cursor(&config, 0, -1),
Movement::NextPage => self.buffer.move_cursor(&config, 0, 25),
Movement::PrevPage => self.buffer.move_cursor(&config, 0, -25),
Movement::FirstLine => self.buffer.to_first_line(&config),
Movement::LastLine => self.buffer.to_last_line(&config),
Movement::NextChar => self.buffer.move_cursor(&config, 1, 0),
Movement::PrevChar => self.buffer.move_cursor(&config, -1, 0),
}
}
pub fn kill_current_line(&mut self) {
let config = self.config.borrow();
self.buffer.kill_line(&config);
}
pub fn erase_backward(&mut self) {
let config = self.config.borrow();
self.buffer.erase_backward(&config);
}
pub fn open_buffer<P: AsRef<Path>>(&mut self, path: P, force: bool) -> Result<(), Error> {
if self.buffer.is_modified() && !force {
return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file"));
}
self.buffer.reopen(path).map_err(Error::OpenError)
}
pub fn write_buffer<P: AsRef<Path>>(
&mut self,
path: Option<P>,
) -> Result<Option<String>, Error> {
if path.is_some() && self.buffer.is_modified() && self.buffer.path().is_some() {
return Err(Error::UnsavedBuffer(
"Use :w! FILE to force write to another file",
));
}
if let Some(path) = path {
self.buffer.set_path(path);
}
self.buffer.save()?;
if let Some(name) = self.buffer.name() {
let status = format!("{name:?} written");
Ok(Some(status))
} else {
Ok(None)
}
}
pub fn wait_for_events(&mut self) -> Result<TermKey, Error> {
self.term.read_key().map_err(Error::TerminalError)
}
pub fn cleanup(&mut self) {
self.term.clear(Clear::All).ok();
}
}