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

256 lines
7.3 KiB
Rust

#![feature(rustc_private, if_let_guard)]
#![allow(clippy::new_without_default)]
use std::{
cell::RefCell,
env,
io::{self, IsTerminal},
path::{Path, PathBuf},
rc::Rc,
};
use libterm::TermKey;
use error::Error;
use lysp::{
error::MachineError,
vm::{
Value,
env::{Environment, EnvironmentAccessHook},
machine::Machine,
value::{
IdentifierValue, StringValue,
convert::{AnyFunction, TryFromValue},
},
},
};
use crate::{config::EditorConfig, script::AsValue, state::State};
pub mod buffer;
pub mod config;
pub mod error;
pub mod highlight;
pub mod keymap;
pub mod state;
pub mod text;
mod script;
#[derive(Clone, Copy, PartialEq)]
pub enum TopMode {
Normal,
Command,
}
pub struct Editor {
machine: Machine,
state: Rc<RefCell<State>>,
env: Rc<Environment>,
}
impl Editor {
pub fn new<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
#[allow(unused)]
struct ConfigAccessHook(Rc<RefCell<State>>, Rc<RefCell<EditorConfig>>);
impl EnvironmentAccessHook for ConfigAccessHook {
fn read_variable(&mut self, name: &str) -> Option<Value> {
let config = self.1.borrow();
match name {
"red/number" => Some((*config.number).into()),
"red/bottom-margin" => Some((*config.bottom_margin).into()),
_ => None,
}
}
fn write_variable(
&mut self,
name: &IdentifierValue,
value: &Value,
) -> Result<bool, MachineError> {
match name.as_ref() {
"red/number" => {
let mut config = self.1.borrow_mut();
*config.number = value.is_trueish();
Ok(true)
}
"red/bottom-margin" => {
let value = usize::try_from_value(value)?;
let mut config = self.1.borrow_mut();
*config.bottom_margin = value;
Ok(true)
}
_ => Ok(false),
}
}
}
let config = Rc::new(RefCell::new(EditorConfig::default()));
let state = Rc::new(RefCell::new(State::open(path, config.clone())?));
let machine = Machine::default();
let env = Rc::new(Environment::default());
env.set_access_hook(Some(Box::new(ConfigAccessHook(state.clone(), config))));
Ok(Self {
state,
machine,
env,
})
}
pub fn defun<S, F>(&self, name: S, function: F)
where
S: Into<IdentifierValue>,
F: Fn(
&mut Machine,
&Rc<Environment>,
&Rc<RefCell<State>>,
&[Value],
) -> Result<Value, Error>
+ 'static,
{
let state = self.state.clone();
self.env.defun_native(name, "", move |vm, env, args| {
match function(vm, env, &state, args) {
Ok(value) => Ok(value),
Err(error) => Err(MachineError::Custom(error.into())),
}
});
}
pub fn defmacro<S, F>(&self, name: S, function: F)
where
S: Into<IdentifierValue>,
F: Fn(
&mut Machine,
&Rc<Environment>,
&Rc<RefCell<State>>,
&[Value],
) -> Result<Value, Error>
+ 'static,
{
let state = self.state.clone();
self.env.defmacro_native(name, "", move |vm, env, args| {
match function(vm, env, &state, args) {
Ok(value) => Ok(value),
Err(error) => Err(MachineError::Custom(error.into())),
}
});
}
pub fn evaluate_file<P: AsRef<Path>>(&mut self, path: P) {
let path = path.as_ref();
if let Err(error) = self.machine.load_file(Default::default(), &self.env, path) {
log::error!("{}: {}", path.display(), error);
self.env
.set_global_value(
"_red/current-message",
Value::String(format!("{}: {}", path.display(), error).into()),
)
.ok();
}
}
pub fn evaluate_callback(&mut self, function: &AnyFunction, args: &[Value]) {
let name = function.name();
if let Err(error) = function.invoke(&mut self.machine, &self.env, args) {
log::error!("{name}: {error}");
self.env
.set_global_value(
"_red/current-message",
Value::String(format!("{name}: {error}").into()),
)
.ok();
}
}
pub fn run(&mut self) -> Result<(), Error> {
script::setup_env(self);
let runtime_directory: PathBuf;
#[cfg(feature = "runtime")]
{
runtime_directory = PathBuf::from("runtime").canonicalize().unwrap();
}
#[cfg(any(not(feature = "runtime"), rust_analyzer))]
{
runtime_directory = "/usr/share/red/runtime".into();
}
self.env
.set_global_value(
"red/runtime-directory",
StringValue::from(format!("{}", runtime_directory.display())),
)
.ok();
self.evaluate_file(runtime_directory.join("core.lysp"));
loop {
let exited = self.state.borrow().exited();
if exited {
break;
}
// TODO pre-render hook
let (width, height) = self.state.borrow_mut().display()?;
let post_render_hook = self.state.borrow().post_render_hook.clone();
if let Some(post_render_hook) = post_render_hook {
self.evaluate_callback(&post_render_hook, &[width.into(), height.into()]);
}
self.state.borrow_mut().finish_display()?;
let key = self.state.borrow_mut().wait_for_events()?;
if let TermKey::Char('Q') = key {
break;
}
let (key_hook, top_mode, buffer_mode) = {
let state = self.state.borrow();
let (top_mode, buffer_mode) = state.mode();
let key_hook = state.key_hook.clone();
(key_hook, top_mode, buffer_mode)
};
if let Some(key_hook) = key_hook {
let key = key.as_value();
let top_mode = top_mode.as_value();
let buffer_mode = buffer_mode.as_value();
self.evaluate_callback(
&key_hook,
&[top_mode.into(), buffer_mode.into(), key.into()],
);
}
}
Ok(())
}
}
fn main() {
logsink::setup_logging(false);
let args = env::args().collect::<Vec<_>>();
if args.len() > 2 {
eprintln!("Usage: red [FILE]");
return;
}
if !io::stdin().is_terminal() {
eprintln!("Not a tty");
return;
}
let path = args.get(1);
let mut editor = Editor::new(path).unwrap();
let result = editor.run();
editor.state.borrow_mut().cleanup();
if let Err(error) = result {
eprintln!("Error: {error}");
}
}