diff --git a/.gitignore b/.gitignore index 8056391..542260a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock *.swp *.swo *.swn +/.vscode diff --git a/config/.oxrc b/config/.oxrc index a12a52a..97cf289 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -226,6 +226,9 @@ Shift + <-: Previous Tab ]] +-- Configure Mouse Behaviour -- +terminal.mouse_enabled = false + -- Configure Syntax Highlighting Colours -- syntax:set("string", {39, 222, 145}) -- Strings in various programming languages syntax:set("comment", {113, 113, 169}) -- Comments in various programming languages diff --git a/src/config.rs b/src/config.rs index e1b9f1b..6910dff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,7 @@ pub struct Config { pub colors: Rc>, pub status_line: Rc>, pub greeting_message: Rc>, + pub terminal: Rc>, } impl Config { @@ -81,6 +82,7 @@ impl Config { let greeting_message = Rc::new(RefCell::new(GreetingMessage::default())); let colors = Rc::new(RefCell::new(Colors::default())); let status_line = Rc::new(RefCell::new(StatusLine::default())); + let terminal = Rc::new(RefCell::new(TerminalConfig::default())); // Push in configuration globals lua.globals().set("syntax", syntax_highlighting.clone())?; @@ -88,6 +90,7 @@ impl Config { lua.globals().set("greeting_message", greeting_message.clone())?; lua.globals().set("status_line", status_line.clone())?; lua.globals().set("colors", colors.clone())?; + lua.globals().set("terminal", terminal.clone())?; Ok(Config { syntax_highlighting, @@ -95,6 +98,7 @@ impl Config { greeting_message, status_line, colors, + terminal, }) } @@ -115,6 +119,30 @@ impl Config { } } +/// For storing general configuration related to the terminal functionality +#[derive(Debug)] +pub struct TerminalConfig { + pub mouse_enabled: bool, +} + +impl Default for TerminalConfig { + fn default() -> Self { + Self { + mouse_enabled: true, + } + } +} + +impl LuaUserData for TerminalConfig { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("mouse_enabled", |_, this| Ok(this.mouse_enabled)); + fields.add_field_method_set("mouse_enabled", |_, this, value| { + this.mouse_enabled = value; + Ok(()) + }); + } +} + /// For storing configuration information related to syntax highlighting #[derive(Debug)] pub struct SyntaxHighlighting { diff --git a/src/editor.rs b/src/editor.rs index 9c50c1b..e27ee21 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -14,10 +14,14 @@ use std::time::Instant; use std::io::{Write, ErrorKind}; use mlua::Lua; +mod mouse; + /// For managing all editing and rendering of cactus pub struct Editor { /// Interface for writing to the terminal pub terminal: Terminal, + /// Whether to rerender the editor on the next cycle + pub needs_rerender: bool, /// Configuration information for the editor pub config: Config, /// Storage of all the documents opened in the editor @@ -43,14 +47,16 @@ pub struct Editor { impl Editor { /// Create a new instance of the editor pub fn new(lua: &Lua) -> Result { + let config = Config::new(lua)?; Ok(Self { doc: vec![], ptr: 0, - terminal: Terminal::new(), - config: Config::new(lua)?, + terminal: Terminal::new(config.terminal.clone()), + config, active: true, greet: false, help: false, + needs_rerender: true, highlighter: vec![], feedback: Feedback::None, command: None, @@ -199,7 +205,15 @@ impl Editor { // Run the editor self.render()?; // Wait for an event - match read()? { + let event = read()?; + self.needs_rerender = match event { + CEvent::Mouse(event) => match event.kind { + crossterm::event::MouseEventKind::Moved => false, + _ => true, + }, + _ => true, + }; + match event { CEvent::Key(key) => { // Check period of inactivity and commit events (for undo/redo) if over 10secs let end = Instant::now(); @@ -234,6 +248,10 @@ impl Editor { self.doc_mut().move_home(); } } + CEvent::Mouse(mouse_event) => { + self.handle_mouse_event(mouse_event); + return Ok(None); + } _ => (), } self.feedback = Feedback::None; @@ -258,6 +276,10 @@ impl Editor { /// Render a single frame of the editor in it's current state pub fn render(&mut self) -> Result<()> { + if !self.needs_rerender { + return Ok(()); + } + self.needs_rerender = false; self.terminal.hide_cursor()?; let Size { w, mut h } = size()?; h = h.saturating_sub(2); @@ -340,6 +362,12 @@ impl Editor { Ok(()) } + fn render_document_tab_header(&self, document: &Document) -> String { + let file_name = document.file_name.clone().unwrap_or_else(|| "[No Name]".to_string()); + let modified = if document.modified { "[+]" } else { "" }; + format!(" {file_name}{modified} ") + } + /// Render the tab line at the top of the document fn render_tab_line(&mut self, w: usize) -> Result<()> { self.terminal.prepare_line(0)?; @@ -350,13 +378,12 @@ impl Editor { Bg(self.config.colors.borrow().tab_inactive_bg.to_color()?) )?; for (c, document) in self.doc.iter().enumerate() { - let file_name = document.file_name.clone().unwrap_or_else(|| "[No Name]".to_string()); - let modified = if document.modified { "[+]" } else { "" }; + let document_header = self.render_document_tab_header(document); if c == self.ptr { // Representing the document we're currently looking at write!( self.terminal.stdout, - "{}{}{} {file_name}{modified} {}{}{}│", + "{}{}{}{document_header}{}{}{}│", Bg(self.config.colors.borrow().tab_active_bg.to_color()?), Fg(self.config.colors.borrow().tab_active_fg.to_color()?), SetAttribute(Attribute::Bold), @@ -366,7 +393,7 @@ impl Editor { )?; } else { // Other document that is currently open - write!(self.terminal.stdout, " {file_name}{modified} │")?; + write!(self.terminal.stdout, "{document_header}│")?; } } write!(self.terminal.stdout, "{}", " ".to_string().repeat(w))?; diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs new file mode 100644 index 0000000..e93683a --- /dev/null +++ b/src/editor/mouse.rs @@ -0,0 +1,60 @@ +use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; +use kaolinite::Loc; + +use super::Editor; + +enum MouseLocation { + File(Loc), + Tabs(usize), + Out, +} + +impl Editor { + fn find_mouse_location(&mut self, event: MouseEvent) -> MouseLocation { + if event.row == 0 { + let mut c = event.column + 2; + for (i, doc) in self.doc.iter().enumerate() { + let header_len = self.render_document_tab_header(doc).len() + 1; + c = c.saturating_sub(header_len as u16); + if c == 0 { + return MouseLocation::Tabs(i); + } + } + MouseLocation::Out + } else if (event.column as usize) < self.dent() { + MouseLocation::Out + } else { + let offset = self.doc().offset; + MouseLocation::File(Loc { x: event.column as usize - self.dent() + offset.x, y: (event.row as usize) - 1 + offset.y }) + } + } + + pub fn handle_mouse_event(&mut self, event: MouseEvent) { + match event.kind { + MouseEventKind::Down(MouseButton::Left) => { + match self.find_mouse_location(event) { + MouseLocation::File(loc) => { + self.doc_mut().goto(&loc); + }, + MouseLocation::Tabs(i) => { + self.ptr = i; + }, + MouseLocation::Out => (), + } + }, + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { + match self.find_mouse_location(event) { + MouseLocation::File(_) => { + if event.kind == MouseEventKind::ScrollDown { + self.doc_mut().move_down(); + } else { + self.doc_mut().move_up(); + } + }, + _ => (), + } + } + _ => (), + } + } +} diff --git a/src/ui.rs b/src/ui.rs index 793f9f2..a674f5b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,14 +1,16 @@ -use crate::config::Colors; +use crate::config::{Colors, TerminalConfig}; use crate::error::Result; use crossterm::{ - cursor::{Hide, MoveTo, Show}, - execute, - style::{SetBackgroundColor as Bg, SetForegroundColor as Fg, SetAttribute, Attribute}, - terminal::{self, Clear, ClearType as ClType, EnterAlternateScreen, LeaveAlternateScreen, EnableLineWrap, DisableLineWrap}, - event::{PushKeyboardEnhancementFlags, KeyboardEnhancementFlags}, + cursor::{Hide, MoveTo, Show}, + event::{EnableMouseCapture, DisableMouseCapture, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, + execute, + style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, + terminal::{self, Clear, ClearType as ClType, DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen} }; -use kaolinite::utils::{Size}; +use kaolinite::utils::Size; +use std::cell::RefCell; use std::io::{stdout, Stdout, Write}; +use std::rc::Rc; /// Constant that shows the help message pub const HELP_TEXT: &str = " @@ -84,12 +86,14 @@ impl Feedback { pub struct Terminal { pub stdout: Stdout, + pub config: Rc>, } impl Terminal { - pub fn new() -> Self { + pub fn new(config: Rc>) -> Self { Terminal { stdout: stdout(), + config, } } @@ -101,6 +105,9 @@ impl Terminal { eprintln!("{}", e); })); execute!(self.stdout, EnterAlternateScreen, Clear(ClType::All), DisableLineWrap)?; + if self.config.borrow().mouse_enabled { + execute!(self.stdout, EnableMouseCapture)?; + } terminal::enable_raw_mode()?; execute!( self.stdout, @@ -115,6 +122,9 @@ impl Terminal { pub fn end(&mut self) -> Result<()> { terminal::disable_raw_mode()?; execute!(self.stdout, LeaveAlternateScreen, EnableLineWrap)?; + if self.config.borrow().mouse_enabled { + execute!(self.stdout, DisableMouseCapture)?; + } Ok(()) }