diff --git a/userspace/tools/red/runtime/command.lysp b/userspace/tools/red/runtime/command.lysp index 4f9d61da..cd2b8339 100644 --- a/userspace/tools/red/runtime/command.lysp +++ b/userspace/tools/red/runtime/command.lysp @@ -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 diff --git a/userspace/tools/red/runtime/core.lysp b/userspace/tools/red/runtime/core.lysp index 56883f25..3e02940b 100644 --- a/userspace/tools/red/runtime/core.lysp +++ b/userspace/tools/red/runtime/core.lysp @@ -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) diff --git a/userspace/tools/red/runtime/keymap/command.lysp b/userspace/tools/red/runtime/keymap/command.lysp index 073e8a56..661b8ea5 100644 --- a/userspace/tools/red/runtime/keymap/command.lysp +++ b/userspace/tools/red/runtime/keymap/command.lysp @@ -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)))) diff --git a/userspace/tools/red/runtime/keymap/normal.lysp b/userspace/tools/red/runtime/keymap/normal.lysp index b5c0ea46..0e9e6aa7 100644 --- a/userspace/tools/red/runtime/keymap/normal.lysp +++ b/userspace/tools/red/runtime/keymap/normal.lysp @@ -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) diff --git a/userspace/tools/red/runtime/message.lysp b/userspace/tools/red/runtime/message.lysp new file mode 100644 index 00000000..38839317 --- /dev/null +++ b/userspace/tools/red/runtime/message.lysp @@ -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)) diff --git a/userspace/tools/red/runtime/render.lysp b/userspace/tools/red/runtime/render.lysp new file mode 100644 index 00000000..9e32d28c --- /dev/null +++ b/userspace/tools/red/runtime/render.lysp @@ -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) diff --git a/userspace/tools/red/src/config.rs b/userspace/tools/red/src/config.rs index adc3e6b2..e3bb970a 100644 --- a/userspace/tools/red/src/config.rs +++ b/userspace/tools/red/src/config.rs @@ -6,6 +6,7 @@ use std::{ pub struct EditorConfig { pub tab_width: usize, pub number: Dirty, + pub bottom_margin: Dirty, } pub struct Dirty(T, bool); @@ -15,6 +16,7 @@ impl Default for EditorConfig { Self { tab_width: 4, number: Dirty::new(false), + bottom_margin: Dirty::new(0), } } } diff --git a/userspace/tools/red/src/main.rs b/userspace/tools/red/src/main.rs index 3ca478a1..7c4b9d52 100644 --- a/userspace/tools/red/src/main.rs +++ b/userspace/tools/red/src/main.rs @@ -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 { 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>(&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(()) diff --git a/userspace/tools/red/src/script/mod.rs b/userspace/tools/red/src/script/mod.rs index 7c5eff21..9c691038 100644 --- a/userspace/tools/red/src/script/mod.rs +++ b/userspace/tools/red/src/script/mod.rs @@ -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()); diff --git a/userspace/tools/red/src/state.rs b/userspace/tools/red/src/state.rs index 1bed84cc..afdfb55a 100644 --- a/userspace/tools/red/src/state.rs +++ b/userspace/tools/red/src/state.rs @@ -15,9 +15,6 @@ use crate::{ pub struct State { pub(super) term: Term, buffer: Buffer, - pub(super) command: String, - message: Option, - status: Option, top_mode: TopMode, config: Rc>, 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>(&mut self, status: S) { - self.status.replace(status.into()); - } - - pub fn set_message>(&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(""); - 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>(&mut self, path: Option

) -> Result<(), Error> { + pub fn write_buffer>( + &mut self, + path: Option

, + ) -> Result, 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 {