red: move more logic into lysp

This commit is contained in:
2026-06-04 17:35:55 +03:00
parent 1736582613
commit d5f70c6a7c
10 changed files with 255 additions and 224 deletions
+14
View File
@@ -1,5 +1,16 @@
(setq _red/command-table (hash/new))
;; Command text manipulation
(setq _red/current-command nil)
(defun red/command/append! (text)
(setq _red/current-command (+ (if _red/current-command _red/current-command "") text)))
(defun red/command/erase-backward! ()
(when _red/current-command
(setq _red/current-command (string/pop _red/current-command))))
(defun red/command/clear! () (setq _red/current-command nil))
;; Command macros
(defun _red/declare-command
(command handler)
(hash/put! _red/command-table command handler))
@@ -21,6 +32,7 @@
)
)
;; Command handlers
(defun _red/editor-command-hook (command)
(when-let
(words (filter identity (string/split command)))
@@ -37,6 +49,7 @@
)
)
;; Root command handler
(defun _red/root-command-hook (command)
(red/set-top-mode 'normal)
(setq command (string/trim command))
@@ -49,6 +62,7 @@
)
)
;; Command definitions
(declare-command "q" () (red/quit))
(declare-command "q!" () (red/quit #t))
(declare-command
+4 -15
View File
@@ -1,23 +1,10 @@
;; External API
(import "util.lysp")
(defun _red/key-sequence-string
(key-seq)
(let (strs (map ->string key-seq))
(string/join strs "+")
)
)
(defun _red/root-post-render-hook (width height)
(unless (nil? _red/key-sequence)
(let (keys (_red/key-sequence-string _red/key-sequence))
(term/set-cursor (- height 1) (- width 10))
(term/write keys)
)
)
)
(import "message.lysp")
;; Bind the hooks
(import "render.lysp")
(red/bind-post-render-hook _red/root-post-render-hook)
;; Child modules
@@ -30,3 +17,5 @@
;; User configuration
(try-import "/etc/red/init.lysp")
(try-import (+ (fs/home-directory) "/.red.d/init.lysp"))
(_red/update-render-params)
@@ -2,16 +2,13 @@
command
('escape (red/set-top-mode 'normal))
('backspace
(let (command (red/command))
(if command
(red/set-command (string/pop command))
(red/set-top-mode 'normal)
)
)
)
(if _red/current-command
(red/command/erase-backward!)
(red/set-top-mode 'normal)
))
('newline
(let (command (red/command))
(red/set-command "")
(let (command _red/current-command)
(red/command/clear!)
(_red/root-command-hook command)
)
)
@@ -24,8 +21,4 @@
(key-seq)
(let (insertable (red/as-insertable-key-seq key-seq))
(unless (nil? insertable)
(let (command (+ (red/command) insertable))
(red/set-command command))
)
)
)
(red/command/append! insertable))))
@@ -1,6 +1,10 @@
(declare-keys
normal
(': (red/set-top-mode 'command))
(':
(red/clear-status)
(red/clear-message)
(red/set-top-mode 'command)
)
('i (red/buffer/set-mode 'insert-before))
('a (red/buffer/set-mode 'insert-after))
('I
@@ -28,6 +32,8 @@
('up (red/buffer/move 'prev-line))
('down (red/buffer/move 'next-line))
('(F F F F F) nil)
('o
(red/buffer/insert-line-after)
(red/buffer/move 'next-line)
+15
View File
@@ -0,0 +1,15 @@
(setq _red/current-message nil)
(setq _red/current-status nil)
(defun _red/set-message (var args)
(let
(value (if (nil? args)
nil
(string/join (map ->string args))))
(set var value)))
(defun red/message (&rest args) (_red/set-message '_red/current-message args))
(defun red/status (&rest args) (_red/set-message '_red/current-status args))
(defun red/clear-message () (setq _red/current-message nil))
(defun red/clear-status () (setq _red/current-status nil))
+112
View File
@@ -0,0 +1,112 @@
;; Drawing options
(setq red/render/status-line #T)
(setq red/render/status-line/key-sequence #T)
(setq red/render/mode-line #T)
(setq red/render/mode-line/arrow #T)
;; Drawing functions
(defun _red/colorize-mode
(mode)
(cond
((= mode 'normal) '(black cyan))
((= mode 'insert) '(black yellow))
((= mode 'command) '(black green))
(&otherwise nil)))
(defun _red/render-mode-line
(row mode name modified)
(unless red/render/mode-line (return))
(term/set-cursor row 0)
(let (color (_red/colorize-mode mode))
(when color
(when (car color)
(term/fg-color (car color)))
(when (cadr color)
(term/bg-color (cadr color))
)
)
(term/write (+ " " (string/to-upper (->string mode)) " "))
;; Invert colors and draw the arrow
(when color
(when (and red/render/mode-line/arrow (cadr color))
(term/fg-color (cadr color))
(term/bg-color 'black)
(term/write "🭬"))
(term/reset)
)
)
;; Render bufer name
(when name
(term/write (+ " " name)))
(when modified
(term/write " [+]"))
)
(defun _red/render-status-line
(row)
(let (message (cond
(_red/current-message (list _red/current-message 'red))
(_red/current-status (list _red/current-status 'white)))
)
(unless message (return))
(term/set-cursor row 0)
(when (cadr message)
(term/fg-color (cadr message)))
(term/write (car message))))
(defun _red/render-key-line
(row width)
(let (keys (string/join (map ->string _red/key-sequence) "+"))
(unless keys (return))
(term/set-cursor row (- width (+ (string/length keys) 1)))
(term/write keys)
))
(defun _red/render-command (row command)
(term/set-cursor row 0)
(term/write ":")
(when command
(term/write command)))
(defun _red/render-bottom-bar
(width height)
(let* (mode (red/buffer/mode) top-mode (car mode) buffer-mode (cadr mode))
(when (= top-mode 'command) (setq buffer-mode 'command))
;; Display mode line
(_red/render-mode-line
(- height _red/mode-line-offset)
buffer-mode
(red/buffer/name)
(red/buffer/modified?))
(when (= top-mode 'normal)
;; Status text
(_red/render-status-line (- height _red/status-line-offset))
;; Current key sequence
(_red/render-key-line (- height _red/status-line-offset) width)
)
(when (= top-mode 'command)
;; Render command
(_red/render-command (- height _red/status-line-offset) _red/current-command))
)
)
(defun _red/root-post-render-hook (width height)
(_red/render-bottom-bar width height)
(term/reset)
;; If not in command mode, set cursor to buffer
(when (= 'normal (car (red/buffer/mode)))
(red/cursor-to-buffer))
)
;; command line + status line
(defun _red/update-render-params ()
(setq _red/status-line-offset 1)
(setq _red/mode-line-offset 2)
(setq red/bottom-margin (+ (and red/render/mode-line 1) 1)))
(_red/update-render-params)
+2
View File
@@ -6,6 +6,7 @@ use std::{
pub struct EditorConfig {
pub tab_width: usize,
pub number: Dirty<bool>,
pub bottom_margin: Dirty<usize>,
}
pub struct Dirty<T>(T, bool);
@@ -15,6 +16,7 @@ impl Default for EditorConfig {
Self {
tab_width: 4,
number: Dirty::new(false),
bottom_margin: Dirty::new(0),
}
}
}
+38 -16
View File
@@ -18,7 +18,10 @@ use lysp::{
Value,
env::{Environment, EnvironmentAccessHook},
machine::Machine,
value::{IdentifierValue, StringValue, convert::AnyFunction},
value::{
IdentifierValue, StringValue,
convert::{AnyFunction, TryFromValue},
},
},
};
@@ -57,17 +60,28 @@ impl Editor {
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) -> bool {
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();
true
Ok(true)
}
_ => false,
"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),
}
}
}
@@ -127,18 +141,26 @@ impl Editor {
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) {
self.state
.borrow_mut()
.set_message(format!("{}: {}", path.display(), error));
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) {
self.state
.borrow_mut()
.set_message(format!("{name}: {error}"));
log::error!("{name}: {error}");
self.env
.set_global_value(
"_red/current-message",
Value::String(format!("{name}: {error}").into()),
)
.ok();
}
}
@@ -155,10 +177,12 @@ impl Editor {
runtime_directory = "/usr/share/red/runtime".into();
}
self.env.set_global_value(
"red/runtime-directory",
StringValue::from(format!("{}", runtime_directory.display())),
);
self.env
.set_global_value(
"red/runtime-directory",
StringValue::from(format!("{}", runtime_directory.display())),
)
.ok();
self.evaluate_file(runtime_directory.join("core.lysp"));
loop {
@@ -200,8 +224,6 @@ impl Editor {
&key_hook,
&[top_mode.into(), buffer_mode.into(), key.into()],
);
} else {
self.state.borrow_mut().set_message("No root key hook set!");
}
}
Ok(())
+36 -52
View File
@@ -10,7 +10,7 @@ use crate::{
pub mod convert;
pub use convert::{AsValue, FromValue, Movement};
use libterm::Color;
use libterm::{Clear, Color};
use lysp::{
error::MachineError,
vm::{
@@ -23,7 +23,7 @@ use lysp::{
};
fn editor_api(editor: &mut Editor) {
editor.defun("red/shell-command", |_, _, state, args| {
editor.defun("red/shell-command", |_, env, _, args| {
let [command, args @ ..] = args else {
return Err(MachineError::InvalidArgumentCount.into());
};
@@ -40,7 +40,9 @@ fn editor_api(editor: &mut Editor) {
if let Ok(text) = std::str::from_utf8(&output.stdout[..]) {
let text = text.trim();
let text = text.rsplit_once('\n').map(|(_, v)| v).unwrap_or(text);
state.borrow_mut().set_status(text);
env.set_global_value("_red/current-status", Value::String(text.into()))
.ok();
}
Ok(Value::Nil)
} else {
@@ -80,55 +82,11 @@ fn editor_api(editor: &mut Editor) {
[] => false,
[arg, ..] => arg.is_trueish(),
};
state.borrow_mut().exit(force);
state.borrow_mut().exit(force)?;
Ok(Value::Nil)
});
editor.defun("red/command", |_, _, state, _| {
let command = state.borrow().command.clone();
Ok(StringValue::from(command).into())
});
editor.defun("red/set-command", |_, _, state, args| {
let [command] = args else {
return Err(MachineError::InvalidArgumentCount.into());
};
let command = StringValue::try_from_value(command)?;
state.borrow_mut().command = (*command).to_owned();
Ok(Value::Nil)
});
editor.defun("red/message", |_, _, state, args| {
let mut message = String::new();
for (i, arg) in args.iter().enumerate() {
if i != 0 {
message.push(' ');
}
match arg {
Value::String(value) => {
message.push_str(value.as_ref());
}
_ => {
message.push_str(&format!("{arg}"));
}
}
}
state.borrow_mut().set_message(message);
Ok(Value::Nil)
});
editor.defun("red/status", |_, _, state, args| {
let mut message = String::new();
for (i, arg) in args.iter().enumerate() {
if i != 0 {
message.push(' ');
}
match arg {
Value::String(value) => {
message.push_str(value.as_ref());
}
_ => {
message.push_str(&format!("{arg}"));
}
}
}
state.borrow_mut().set_status(message);
editor.defun("red/cursor-to-buffer", |_, _, state, _| {
state.borrow_mut().cursor_to_buffer().ok();
Ok(Value::Nil)
});
}
@@ -288,6 +246,15 @@ fn buffer_api(editor: &mut Editor) {
Ok(Value::Nil)
}
});
editor.defun("red/buffer/name", |_, _, state, _| {
let state = state.borrow();
let name = state.buffer().name();
if let Some(name) = name {
Ok(Value::String((&**name).into()))
} else {
Ok(Value::Nil)
}
});
editor.defun("red/buffer/mode", |_, _, state, _| {
let state = state.borrow();
let (top_mode, buffer_mode) = state.mode();
@@ -304,14 +271,17 @@ fn buffer_api(editor: &mut Editor) {
state.set_mode(target_mode);
Ok(Value::Nil)
});
editor.defun("red/buffer/write", |_, _, state, args| {
editor.defun("red/buffer/write", |_, env, state, args| {
let path = match args {
[] => None,
[path] => Some(StringValue::try_from_value(path)?),
_ => return Err(MachineError::InvalidArgumentCount.into()),
};
let mut state = state.borrow_mut();
state.write_buffer(path.as_deref())?;
if let Some(status) = state.write_buffer(path.as_deref())? {
env.set_global_value("_red/current-status", Value::String(status.into()))
.ok();
}
Ok(Value::Nil)
});
editor.defun("red/buffer/open", |_, _, state, args| {
@@ -358,6 +328,10 @@ fn buffer_api(editor: &mut Editor) {
state.buffer_mut().newline_after(break_line);
Ok(Value::Nil)
});
editor.defun("red/buffer/modified?", |_, _, state, _| {
let state = state.borrow();
Ok(state.buffer().is_modified().into())
});
editor.defun("red/buffer/kill-line", |_, _, state, _| {
let mut state = state.borrow_mut();
state.kill_current_line();
@@ -378,6 +352,16 @@ fn buffer_api(editor: &mut Editor) {
}
fn term_api(editor: &mut Editor) {
editor.defun("term/reset", |_, _, state, _| {
let mut state = state.borrow_mut();
state.term.reset_style().ok();
Ok(Value::Nil)
});
editor.defun("term/clear-line", |_, _, state, _| {
let mut state = state.borrow_mut();
state.term.clear(Clear::LineToEnd).ok();
Ok(Value::Nil)
});
editor.defun("term/fg-color", |_, _, state, args| {
let [color] = args else {
return Err(MachineError::InvalidArgumentCount.into());
+20 -126
View File
@@ -15,9 +15,6 @@ use crate::{
pub struct State {
pub(super) term: Term,
buffer: Buffer,
pub(super) command: String,
message: Option<String>,
status: Option<String>,
top_mode: TopMode,
config: Rc<RefCell<EditorConfig>>,
running: bool,
@@ -43,9 +40,6 @@ impl State {
Ok(Self {
number_width: buffer.number_width(),
top_mode: TopMode::Normal,
message: None,
status: None,
command: String::new(),
running: true,
buffer,
term,
@@ -76,26 +70,19 @@ impl State {
Some(self.buffer.get_terminal_cursor(&self.config.borrow()))
}
pub fn exit(&mut self, force: bool) {
pub fn exit(&mut self, force: bool) -> Result<(), Error> {
if self.buffer.is_modified() && !force {
self.message = Some("Buffer has unsaved changes. Use :q! to force-exit".into());
return;
Err(Error::UnsavedBuffer("Use :q! to force-exit"))
} else {
self.running = false;
Ok(())
}
self.running = false;
}
pub fn exited(&self) -> bool {
!self.running
}
pub fn set_status<S: Into<String>>(&mut self, status: S) {
self.status.replace(status.into());
}
pub fn set_message<S: Into<String>>(&mut self, message: S) {
self.message.replace(message.into());
}
fn display_number(&mut self) -> Result<(), Error> {
let start = self.buffer.row_offset();
let end = self.buffer.len();
@@ -131,85 +118,12 @@ impl State {
Ok(())
}
fn display_modeline(&mut self) -> Result<(), Error> {
self.term
.set_cursor_position(self.buffer.height(), 0)
.map_err(Error::TerminalError)?;
let bg = match (self.top_mode, self.buffer.mode()) {
(TopMode::Normal, Mode::Normal) => Color::Yellow,
(TopMode::Normal, Mode::Insert) => Color::Cyan,
(TopMode::Command, _) => Color::Green,
};
self.term.set_background(bg).map_err(Error::TerminalError)?;
self.term
.set_foreground(Color::Black)
.map_err(Error::TerminalError)?;
match self.top_mode {
TopMode::Normal => {
write!(self.term, " {} ", self.buffer.mode().as_str())
.map_err(Error::TerminalFmtError)?;
if self.buffer.is_modified() {
self.term
.set_background(Color::Magenta)
.map_err(Error::TerminalError)?;
self.term
.set_foreground(Color::Default)
.map_err(Error::TerminalError)?;
} else {
self.term
.set_foreground(Color::Green)
.map_err(Error::TerminalError)?;
self.term
.set_background(Color::Default)
.map_err(Error::TerminalError)?;
}
}
TopMode::Command => {
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
self.term
.set_foreground(Color::Green)
.map_err(Error::TerminalError)?;
self.term
.set_background(Color::Default)
.map_err(Error::TerminalError)?;
}
}
let name = self
.buffer
.name()
.map(String::as_str)
.unwrap_or("<unnamed>");
write!(self.term, " {}", name).map_err(Error::TerminalFmtError)?;
self.term
.clear(Clear::LineToEnd)
.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> {
let config = self.config.borrow();
match self.top_mode {
TopMode::Normal => {
self.buffer.set_terminal_cursor(&config, &mut self.term)?;
}
TopMode::Command => {
self.term
.set_cursor_position(self.buffer.height() + 1, 0)
.map_err(Error::TerminalError)?;
write!(self.term, ":{}", self.command.as_str()).map_err(Error::TerminalFmtError)?;
}
}
self.term.flush().map_err(Error::TerminalError)?;
Ok(())
}
@@ -224,12 +138,14 @@ impl State {
let (w, h) = self.term.size().map_err(Error::TerminalError)?;
if config.number.clean() {
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 - 2);
self.buffer
.resize(&config, nw, w - nw - 1, h - (*config.bottom_margin + 1));
} else {
self.buffer.resize(&config, 0, w - 1, h - 2);
self.buffer
.resize(&config, 0, w - 1, h - (*config.bottom_margin + 1));
}
}
@@ -240,34 +156,11 @@ impl State {
self.buffer
.display(&config, &mut self.term, &self.highlight)?;
if self.top_mode != TopMode::Command
&& let Some(status) = &self.status
{
self.term
.set_cursor_position(self.buffer().height() + 1, 0)
.map_err(Error::TerminalError)?;
self.term
.write_str(status.as_str())
.map_err(Error::TerminalFmtError)?;
}
if let Some(msg) = &self.message {
self.term
.set_cursor_position(self.buffer.height(), 0)
.map_err(Error::TerminalError)?;
self.term.write_str(msg).map_err(Error::TerminalFmtError)?;
self.term.flush().map_err(Error::TerminalError)?;
return Ok((w, h));
}
self.display_modeline()?;
Ok((w, h))
}
pub fn set_mode(&mut self, target: SetMode) {
self.top_mode = TopMode::Normal;
self.message = None;
self.buffer.set_mode(&self.config.borrow(), target);
}
@@ -277,8 +170,6 @@ impl State {
}
self.top_mode = target;
self.message = None;
self.command.clear();
self.buffer.set_mode(&self.config.borrow(), SetMode::Normal);
}
@@ -323,7 +214,10 @@ impl State {
self.buffer.reopen(path).map_err(Error::OpenError)
}
pub fn write_buffer<P: AsRef<Path>>(&mut self, path: Option<P>) -> Result<(), Error> {
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",
@@ -337,10 +231,10 @@ impl State {
if let Some(name) = self.buffer.name() {
let status = format!("{name:?} written");
self.set_status(status);
Ok(Some(status))
} else {
Ok(None)
}
Ok(())
}
pub fn wait_for_events(&mut self) -> Result<TermKey, Error> {