From 114c46a2ed8586e2c2cce353c455b1d3e3417f3a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:20:26 +0000 Subject: [PATCH 01/22] More accurate old_ptr caching --- src/config/editor.rs | 15 +++++++++++++-- src/editor/mouse.rs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 979a9f13..67ddc206 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -594,6 +594,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_up(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(true) } else { @@ -605,6 +606,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_down(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(true) } else { @@ -616,6 +618,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_left(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(true) } else { @@ -627,6 +630,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_right(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(true) } else { @@ -657,11 +661,13 @@ impl LuaUserData for Editor { ); methods.add_method_mut("focus_split_up", |_, editor, ()| { editor.ptr = FileLayout::move_up(editor.ptr.clone(), &editor.render_cache.span); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(()) }); methods.add_method_mut("focus_split_down", |_, editor, ()| { editor.ptr = FileLayout::move_down(editor.ptr.clone(), &editor.render_cache.span); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(()) }); @@ -672,9 +678,13 @@ impl LuaUserData for Editor { Some(FileLayout::FileTree) ); if in_file_tree { - // We just entered into a file tree, cache where we were (minus the file tree itself) + // We are about to enter a file tree, cache where we were (minus the file tree itself) editor.old_ptr = editor.ptr.clone(); - editor.old_ptr.pop(); + } else { + editor.old_ptr = new_ptr.clone(); + } + if editor.file_tree_is_open() && !editor.old_ptr.is_empty() { + editor.old_ptr.remove(0); } editor.ptr = new_ptr; editor.update_cwd(); @@ -682,6 +692,7 @@ impl LuaUserData for Editor { }); methods.add_method_mut("focus_split_right", |_, editor, ()| { editor.ptr = FileLayout::move_right(editor.ptr.clone(), &editor.render_cache.span); + editor.cache_old_ptr(&editor.ptr.clone()); editor.update_cwd(); Ok(()) }); diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index d3c7a55e..c261eb64 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -312,7 +312,7 @@ impl Editor { } /// Cache the old ptr - fn cache_old_ptr(&mut self, idx: &Vec) { + pub fn cache_old_ptr(&mut self, idx: &Vec) { self.old_ptr.clone_from(idx); if self.file_tree_is_open() && !self.old_ptr.is_empty() { self.old_ptr.remove(0); From ad2af0734a400058113b17dbe109dc4c1a06d016 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:30:41 +0000 Subject: [PATCH 02/22] Added pseudoterminal utility --- Cargo.lock | 52 +++++++++++++++++++++++++++--- Cargo.toml | 1 + src/main.rs | 1 + src/pty.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 src/pty.rs diff --git a/Cargo.lock b/Cargo.lock index 957611b4..60af9234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -81,7 +87,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "mio", "parking_lot", @@ -220,7 +226,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", ] @@ -271,6 +277,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "mio" version = "1.0.3" @@ -310,6 +325,19 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -342,6 +370,7 @@ dependencies = [ "jargon-args", "kaolinite", "mlua", + "ptyprocess", "shellexpand", "synoptic", ] @@ -369,6 +398,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.31" @@ -393,6 +428,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptyprocess" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e05aef7befb11a210468a2d77d978dde2c6381a0381e33beb575e91f57fe8cf" +dependencies = [ + "nix", +] + [[package]] name = "quote" version = "1.0.37" @@ -438,7 +482,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -503,7 +547,7 @@ version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", diff --git a/Cargo.toml b/Cargo.toml index 8f6f41f3..10881e86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,4 @@ mlua = { version = "0.10", features = ["lua54", "vendored"] } error_set = "0.7" shellexpand = "3.1.0" synoptic = "2.2.9" +ptyprocess = "0.4.1" diff --git a/src/main.rs b/src/main.rs index 79025a2c..7e8878be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod config; mod editor; mod error; mod events; +mod pty; mod ui; use cli::CommandLineInterface; diff --git a/src/pty.rs b/src/pty.rs new file mode 100644 index 00000000..094b72a4 --- /dev/null +++ b/src/pty.rs @@ -0,0 +1,91 @@ +/// User friendly interface for dealing with pseudo terminals +use ptyprocess::PtyProcess; +use std::io::{BufReader, Read, Result, Write}; +use std::process::Command; + +pub struct Pty { + pub process: PtyProcess, + pub output: String, + pub input: String, + pub shell: Shell, +} + +pub enum Shell { + Bash, + Dash, + Zsh, + Fish, +} + +impl Shell { + pub fn manual_input_echo(&self) -> bool { + matches!(self, Self::Bash | Self::Dash) + } + + pub fn inserts_extra_newline(&self) -> bool { + !matches!(self, Self::Zsh) + } + + pub fn command(&self) -> &str { + match self { + Self::Bash => "bash", + Self::Dash => "dash", + Self::Zsh => "zsh", + Self::Fish => "fish", + } + } +} + +impl Pty { + pub fn new(shell: Shell) -> Result { + let mut pty = Self { + process: PtyProcess::spawn(Command::new(shell.command()))?, + output: String::new(), + input: String::new(), + shell, + }; + pty.process.set_echo(false, None)?; + std::thread::sleep(std::time::Duration::from_millis(100)); + pty.run_command("")?; + Ok(pty) + } + + pub fn run_command(&mut self, cmd: &str) -> Result<()> { + let mut stream = self.process.get_raw_handle()?; + // Write the command + write!(stream, "{cmd}")?; + std::thread::sleep(std::time::Duration::from_millis(100)); + if self.shell.manual_input_echo() { + // println!("Adding (pre-cmd) {:?}", cmd); + self.output += &cmd; + } + // Read the output + let mut reader = BufReader::new(stream); + let mut buf = [0u8; 1024]; + let bytes_read = reader.read(&mut buf)?; + let mut output = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); + // Add on the output + if self.shell.inserts_extra_newline() { + output = output.replace("\u{1b}[?2004l\r\r\n", ""); + } + // println!("Adding (aftercmd) \"{:?}\"", output); + self.output += &output; + Ok(()) + } + + pub fn char_input(&mut self, c: char) -> Result<()> { + self.input.push(c); + if c == '\n' { + // Return key pressed, send the input + self.run_command(&self.input.to_string())?; + self.input.clear(); + } + Ok(()) + } +} + +impl Drop for Pty { + fn drop(&mut self) { + let _ = self.process.exit(true); + } +} From 465c2b5244d435dd03d14259cbdee0d2dd8de496 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:50:35 +0000 Subject: [PATCH 03/22] Integrated terminal into the split system --- src/editor/documents.rs | 64 +++++++++++++++++++++++------------------ src/editor/filetree.rs | 8 ++++-- src/pty.rs | 8 ++---- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 9551c016..97ce4bf2 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -1,5 +1,6 @@ /// Tools for placing all information about open files into one place use crate::editor::{get_absolute_path, Editor, FileType}; +use crate::pty::Pty; use crate::Loc; use kaolinite::Document; use kaolinite::Size; @@ -9,7 +10,7 @@ use synoptic::Highlighter; pub type Span = Vec<(Vec, Range, Range)>; // File split structure -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum FileLayout { /// Side-by-side documents (with proportions) SideBySide(Vec<(FileLayout, f64)>), @@ -21,6 +22,8 @@ pub enum FileLayout { None, /// Representing a file tree FileTree, + /// Representing a terminal + Terminal(Pty), } impl Default for FileLayout { @@ -40,8 +43,8 @@ impl FileLayout { pub fn span(&self, idx: Vec, size: Size, at: Loc) -> Span { match self { Self::None => vec![], - // Atom and file trees: stretch from starting position through to end of their containers - Self::Atom(_, _) | Self::FileTree => { + // Atom file trees and terminals: stretch from starting position through to end of their containers + Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => { vec![(idx, at.y..at.y + size.h, at.x..at.x + size.w)] } // SideBySide: distributes available container space to each sub-layout @@ -157,7 +160,7 @@ impl FileLayout { /// Work out how many files are currently open pub fn len(&self) -> usize { match self { - Self::None | Self::FileTree => 0, + Self::None | Self::FileTree | Self::Terminal(_) => 0, Self::Atom(containers, _) => containers.len(), Self::SideBySide(layouts) => layouts.iter().map(|(layout, _)| layout.len()).sum(), Self::TopToBottom(layouts) => layouts.iter().map(|(layout, _)| layout.len()).sum(), @@ -167,7 +170,7 @@ impl FileLayout { /// Work out how many atoms are currently open pub fn n_atoms(&self) -> usize { match self { - Self::None | Self::FileTree => 0, + Self::None | Self::FileTree | Self::Terminal(_) => 0, Self::Atom(_, _) => 1, Self::SideBySide(layouts) => layouts.iter().map(|(layout, _)| layout.n_atoms()).sum(), Self::TopToBottom(layouts) => layouts.iter().map(|(layout, _)| layout.n_atoms()).sum(), @@ -177,7 +180,7 @@ impl FileLayout { /// Find a file container location from it's path pub fn find(&self, idx: Vec, path: &str) -> Option<(Vec, usize)> { match self { - Self::None | Self::FileTree => None, + Self::None | Self::FileTree | Self::Terminal(_) => None, Self::Atom(containers, _) => { // Scan this atom for any documents for (ptr, container) in containers.iter().enumerate() { @@ -207,7 +210,7 @@ impl FileLayout { /// Get the `FileLayout` at a certain index pub fn get_raw(&self, mut idx: Vec) -> Option<&FileLayout> { match self { - Self::None | Self::Atom(_, _) | Self::FileTree => Some(self), + Self::None | Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => Some(self), Self::SideBySide(layouts) => { if idx.is_empty() { Some(self) @@ -233,7 +236,7 @@ impl FileLayout { Some(self) } else { match self { - Self::None | Self::Atom(_, _) | Self::FileTree => Some(self), + Self::None | Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => Some(self), Self::SideBySide(layouts) => { let subidx = idx.remove(0); layouts.get_mut(subidx)?.0.get_raw_mut(idx) @@ -249,7 +252,7 @@ impl FileLayout { /// Get the `FileLayout` at a certain index pub fn set(&mut self, mut idx: Vec, fl: FileLayout) { match self { - Self::None | Self::Atom(_, _) | Self::FileTree => *self = fl, + Self::None | Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => *self = fl, Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { if idx.is_empty() { *self = fl; @@ -264,7 +267,7 @@ impl FileLayout { /// Given an index, find the file containers in the tree pub fn get_atom(&self, mut idx: Vec) -> Option<(&[FileContainer], usize)> { match self { - Self::None | Self::FileTree => None, + Self::None | Self::FileTree | Self::Terminal(_) => None, Self::Atom(containers, ptr) => Some((containers, *ptr)), Self::SideBySide(layouts) => { let subidx = idx.remove(0); @@ -283,7 +286,7 @@ impl FileLayout { mut idx: Vec, ) -> Option<(&mut Vec, &mut usize)> { match self { - Self::None | Self::FileTree => None, + Self::None | Self::FileTree | Self::Terminal(_) => None, Self::Atom(ref mut containers, ref mut ptr) => Some((containers, ptr)), Self::SideBySide(layouts) => { let subidx = idx.remove(0); @@ -316,7 +319,7 @@ impl FileLayout { /// In the currently active atom, move to a different document pub fn move_to(&mut self, mut idx: Vec, ptr: usize) { match self { - Self::None | Self::FileTree => (), + Self::None | Self::FileTree | Self::Terminal(_) => (), Self::Atom(_, ref mut old_ptr) => *old_ptr = ptr, Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { let subidx = idx.remove(0); @@ -341,7 +344,8 @@ impl FileLayout { let multi = self.get_raw_mut(redundant_idx.clone()); if let Some(layout) = multi { if let Self::SideBySide(layouts) | Self::TopToBottom(layouts) = layout { - *layout = layouts[0].0.clone(); + let retain = std::mem::take(&mut layouts[0].0); + *layout = retain; if idx.starts_with(&redundant_idx) { idx.remove(redundant_idx.len()); return idx; @@ -361,7 +365,7 @@ impl FileLayout { // Determine behaviour based on parent if let Some(parent) = self.get_raw_mut(at_parent) { match parent { - Self::None | Self::Atom(_, _) | Self::FileTree => unreachable!(), + Self::None | Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => unreachable!(), Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { // Get the proportion of what we're removing let removed_prop = layouts[within_parent].1; @@ -385,7 +389,7 @@ impl FileLayout { /// Traverse the tree and return a list of indices to empty atoms pub fn empty_atoms(&self, at: Vec) -> Option> { match self { - Self::None | Self::FileTree => None, + Self::None | Self::FileTree | Self::Terminal(_) => None, Self::Atom(fcs, _) => { if fcs.is_empty() { Some(at) @@ -413,7 +417,7 @@ impl FileLayout { /// Traverse the tree and return a list of indices to redundant sidebyside/toptobottom pub fn redundant_multis(&self, at: Vec) -> Option> { match self { - Self::None | Self::FileTree | Self::Atom(_, _) => None, + Self::None | Self::FileTree | Self::Atom(_, _) | Self::Terminal(_) => None, Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { if layouts.len() == 1 { Some(at) @@ -453,12 +457,13 @@ impl FileLayout { /// Open a split above the current pointer pub fn open_up(&mut self, at: Vec, fl: FileLayout) -> Vec { let mut new_ptr = at.clone(); - if let Some(old_fl) = self.get_raw(at.clone()) { + if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { new_ptr.push(0); - Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) + let old_fl = std::mem::replace(old_fl, FileLayout::None); + Self::TopToBottom(vec![(fl, 0.5), (old_fl, 0.5)]) } Self::FileTree => return at, }; @@ -470,12 +475,13 @@ impl FileLayout { /// Open a split below the current pointer pub fn open_down(&mut self, at: Vec, fl: FileLayout) -> Vec { let mut new_ptr = at.clone(); - if let Some(old_fl) = self.get_raw(at.clone()) { + if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { new_ptr.push(1); - Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) + let old_fl = std::mem::replace(old_fl, FileLayout::None); + Self::TopToBottom(vec![(old_fl, 0.5), (fl, 0.5)]) } Self::FileTree => return at, }; @@ -487,12 +493,13 @@ impl FileLayout { /// Open a split to the left of the current pointer pub fn open_left(&mut self, at: Vec, fl: FileLayout) -> Vec { let mut new_ptr = at.clone(); - if let Some(old_fl) = self.get_raw(at.clone()) { + if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { new_ptr.push(0); - Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) + let old_fl = std::mem::replace(old_fl, FileLayout::None); + Self::SideBySide(vec![(fl, 0.5), (old_fl, 0.5)]) } Self::FileTree => return at, }; @@ -504,12 +511,13 @@ impl FileLayout { /// Open a split to the right of the current pointer pub fn open_right(&mut self, at: Vec, fl: FileLayout) -> Vec { let mut new_ptr = at.clone(); - if let Some(old_fl) = self.get_raw(at.clone()) { + if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { new_ptr.push(1); - Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) + let old_fl = std::mem::replace(old_fl, FileLayout::None); + Self::SideBySide(vec![(old_fl, 0.5), (fl, 0.5)]) } Self::FileTree => return at, }; diff --git a/src/editor/filetree.rs b/src/editor/filetree.rs index b73dd23a..447739ef 100644 --- a/src/editor/filetree.rs +++ b/src/editor/filetree.rs @@ -390,9 +390,10 @@ impl Editor { } } // Wrap existing file layout in new file layout + let files = std::mem::take(&mut self.files); self.files = FileLayout::SideBySide(vec![ (FileLayout::FileTree, width), - (self.files.clone(), other), + (files, other), ]); self.ptr = vec![0]; } @@ -413,10 +414,11 @@ impl Editor { // Delete the file tree self.files.remove(vec![at]); // Clear up any leftovers sidebyside - if let FileLayout::SideBySide(layouts) = &self.files { + if let FileLayout::SideBySide(layouts) = &mut self.files { if layouts.len() == 1 { // Remove leftover - self.files = layouts[0].0.clone(); + let layout = std::mem::take(&mut layouts[0].0); + self.files = layout; } } // Reset pointer back to what it used to be IF we're in the file tree diff --git a/src/pty.rs b/src/pty.rs index 094b72a4..111e6ec0 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -3,6 +3,7 @@ use ptyprocess::PtyProcess; use std::io::{BufReader, Read, Result, Write}; use std::process::Command; +#[derive(Debug)] pub struct Pty { pub process: PtyProcess, pub output: String, @@ -10,6 +11,7 @@ pub struct Pty { pub shell: Shell, } +#[derive(Debug)] pub enum Shell { Bash, Dash, @@ -83,9 +85,3 @@ impl Pty { Ok(()) } } - -impl Drop for Pty { - fn drop(&mut self) { - let _ = self.process.exit(true); - } -} From 3fa155d82777c0446f6e1bc2086ab80e3cc1a702 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:51:12 +0000 Subject: [PATCH 04/22] Version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/editor/documents.rs | 24 +++++++++++++++++++----- src/editor/filetree.rs | 6 ++---- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60af9234..8a5cd04b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,7 +361,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.7.4" +version = "0.7.5" dependencies = [ "alinio", "base64", diff --git a/Cargo.toml b/Cargo.toml index 10881e86..5e032b7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [package] name = "ox" -version = "0.7.4" +version = "0.7.5" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A simple but flexible text editor." diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 97ce4bf2..a9dfafbe 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -365,7 +365,9 @@ impl FileLayout { // Determine behaviour based on parent if let Some(parent) = self.get_raw_mut(at_parent) { match parent { - Self::None | Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => unreachable!(), + Self::None | Self::Atom(_, _) | Self::FileTree | Self::Terminal(_) => { + unreachable!() + } Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { // Get the proportion of what we're removing let removed_prop = layouts[within_parent].1; @@ -460,7 +462,10 @@ impl FileLayout { if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { + Self::Atom(_, _) + | Self::SideBySide(_) + | Self::TopToBottom(_) + | Self::Terminal(_) => { new_ptr.push(0); let old_fl = std::mem::replace(old_fl, FileLayout::None); Self::TopToBottom(vec![(fl, 0.5), (old_fl, 0.5)]) @@ -478,7 +483,10 @@ impl FileLayout { if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { + Self::Atom(_, _) + | Self::SideBySide(_) + | Self::TopToBottom(_) + | Self::Terminal(_) => { new_ptr.push(1); let old_fl = std::mem::replace(old_fl, FileLayout::None); Self::TopToBottom(vec![(old_fl, 0.5), (fl, 0.5)]) @@ -496,7 +504,10 @@ impl FileLayout { if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { + Self::Atom(_, _) + | Self::SideBySide(_) + | Self::TopToBottom(_) + | Self::Terminal(_) => { new_ptr.push(0); let old_fl = std::mem::replace(old_fl, FileLayout::None); Self::SideBySide(vec![(fl, 0.5), (old_fl, 0.5)]) @@ -514,7 +525,10 @@ impl FileLayout { if let Some(old_fl) = self.get_raw_mut(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) | Self::Terminal(_) => { + Self::Atom(_, _) + | Self::SideBySide(_) + | Self::TopToBottom(_) + | Self::Terminal(_) => { new_ptr.push(1); let old_fl = std::mem::replace(old_fl, FileLayout::None); Self::SideBySide(vec![(old_fl, 0.5), (fl, 0.5)]) diff --git a/src/editor/filetree.rs b/src/editor/filetree.rs index 447739ef..5e8b0323 100644 --- a/src/editor/filetree.rs +++ b/src/editor/filetree.rs @@ -391,10 +391,8 @@ impl Editor { } // Wrap existing file layout in new file layout let files = std::mem::take(&mut self.files); - self.files = FileLayout::SideBySide(vec![ - (FileLayout::FileTree, width), - (files, other), - ]); + self.files = + FileLayout::SideBySide(vec![(FileLayout::FileTree, width), (files, other)]); self.ptr = vec![0]; } } From 2286f1da29f638400221b9b589f4410deaea0d1e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:22:42 +0000 Subject: [PATCH 05/22] very basic terminal opening and rendering --- Cargo.lock | 1 + Cargo.toml | 1 + config/.oxrc | 24 +++++++++-- src/config/editor.rs | 46 +++++++++++++++++++++ src/editor/interface.rs | 88 ++++++++++++++++++++++++++++++++++++----- src/pty.rs | 2 +- src/ui.rs | 57 ++++++++++++++++++++++++++ 7 files changed, 205 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a5cd04b..1b01804a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "kaolinite", "mlua", "ptyprocess", + "regex", "shellexpand", "synoptic", ] diff --git a/Cargo.toml b/Cargo.toml index 5e032b7a..b115764c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ error_set = "0.7" shellexpand = "3.1.0" synoptic = "2.2.9" ptyprocess = "0.4.1" +regex = "1.11.1" diff --git a/config/.oxrc b/config/.oxrc index f72ce6f8..f5c12039 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -329,13 +329,29 @@ commands = { local file = arguments[2] local result = false if arguments[1] == "left" then - result = editor:open_split_left(file) + if arguments[2] == "terminal" then + result = editor:open_terminal_left() + else + result = editor:open_split_left(file) + end elseif arguments[1] == "right" then - result = editor:open_split_right(file) + if arguments[2] == "terminal" then + result = editor:open_terminal_right() + else + result = editor:open_split_right(file) + end elseif arguments[1] == "up" then - result = editor:open_split_up(file) + if arguments[2] == "terminal" then + result = editor:open_terminal_up() + else + result = editor:open_split_up(file) + end elseif arguments[1] == "down" then - result = editor:open_split_down(file) + if arguments[2] == "terminal" then + result = editor:open_terminal_down() + else + result = editor:open_split_down(file) + end elseif arguments[1] == "grow" then result = true local amount = tonumber(arguments[3]) or 0.15 diff --git a/src/config/editor.rs b/src/config/editor.rs index 67ddc206..f3fc9f14 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -1,6 +1,7 @@ /// Defines the Editor API for plug-ins to use use crate::cli::VERSION; use crate::editor::{Editor, FileContainer, FileLayout}; +use crate::pty::{Pty, Shell}; use crate::ui::Feedback; use crate::{config, fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; @@ -781,6 +782,51 @@ impl LuaUserData for Editor { editor.toggle_file_tree(); Ok(()) }); + // Terminal + methods.add_method_mut("open_terminal_up", |_, editor, ()| { + if let Ok(term) = Pty::new(Shell::Bash) { + editor.ptr = editor + .files + .open_up(editor.ptr.clone(), FileLayout::Terminal(term)); + editor.cache_old_ptr(&editor.ptr.clone()); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("open_terminal_down", |_, editor, ()| { + if let Ok(term) = Pty::new(Shell::Bash) { + editor.ptr = editor + .files + .open_down(editor.ptr.clone(), FileLayout::Terminal(term)); + editor.cache_old_ptr(&editor.ptr.clone()); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("open_terminal_left", |_, editor, ()| { + if let Ok(term) = Pty::new(Shell::Bash) { + editor.ptr = editor + .files + .open_left(editor.ptr.clone(), FileLayout::Terminal(term)); + editor.cache_old_ptr(&editor.ptr.clone()); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("open_terminal_right", |_, editor, ()| { + if let Ok(mut term) = Pty::new(Shell::Bash) { + editor.ptr = editor + .files + .open_right(editor.ptr.clone(), FileLayout::Terminal(term)); + editor.cache_old_ptr(&editor.ptr.clone()); + Ok(true) + } else { + Ok(false) + } + }); // Miscellaneous methods.add_method_mut("open_command_line", |_, editor, ()| { match editor.prompt("Command") { diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 9031bfad..0ea595b2 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -3,7 +3,7 @@ use crate::config::SyntaxHighlighting as SH; use crate::editor::{FTParts, FileLayout}; use crate::error::{OxError, Result}; use crate::events::wait_for_event_hog; -use crate::ui::{key_event, size, Feedback}; +use crate::ui::{key_event, remove_ansi_codes, size, strip_escape_codes, Feedback}; use crate::{config, display, handle_lua_error}; use crossterm::{ event::{KeyCode as KCode, KeyModifiers as KMod}, @@ -26,6 +26,7 @@ pub struct RenderCache { pub help_message_span: Range, pub file_tree: FTParts, pub file_tree_selection: Option, + pub term_cursor: Option, } impl Editor { @@ -65,6 +66,8 @@ impl Editor { self.render_cache.file_tree = files; self.render_cache.file_tree_selection = sel; } + // Clear the terminal cursor position + self.render_cache.term_cursor = None; } /// Render a specific line @@ -83,6 +86,10 @@ impl Editor { self.files.get_raw(fc.to_owned()), Some(FileLayout::FileTree) ); + let in_terminal = matches!( + self.files.get_raw(fc.to_owned()), + Some(FileLayout::Terminal(_)) + ); // Check if we have encountered an area of discontinuity in the line if range.start != accounted_for { // Discontinuity detected, fill with vertical bar! @@ -114,6 +121,9 @@ impl Editor { if in_file_tree { // Part of file tree! result += &self.render_file_tree(y, length)?; + } else if in_terminal { + // Part of terminal! + result += &self.render_terminal(fc, y, length, height)?; } else if y == rows.start && tab_line_enabled { // Tab line result += &self.render_tab_line(fc, lua, length)?; @@ -226,16 +236,37 @@ impl Editor { self.files.get_raw(self.ptr.clone()), Some(FileLayout::FileTree) ); - if !in_file_tree { - let Loc { x, y } = self.try_doc().unwrap().cursor_loc_in_screen()?; - for (ptr, rows, cols) in &self.render_cache.span { - if ptr == &self.ptr { - return Some(Loc { - x: cols.start + x + self.dent(), - y: rows.start + y + self.push_down, - }); + let in_terminal = matches!( + self.files.get_raw(self.ptr.clone()), + Some(FileLayout::Terminal(_)) + ); + match (in_file_tree, in_terminal) { + // Move cursor to location within file + (false, false) => { + let Loc { x, y } = self.try_doc().unwrap().cursor_loc_in_screen()?; + for (ptr, rows, cols) in &self.render_cache.span { + if ptr == &self.ptr { + return Some(Loc { + x: cols.start + x + self.dent(), + y: rows.start + y + self.push_down, + }); + } + } + } + // Move cursor to location within a terminal + (false, true) => { + if let Some(loc) = self.render_cache.term_cursor { + for (ptr, rows, cols) in &self.render_cache.span { + if ptr == &self.ptr { + return Some(Loc { + x: cols.start + loc.x, + y: rows.start + loc.y, + }); + } + } } } + _ => (), } None } @@ -604,6 +635,45 @@ impl Editor { } } + /// Render the line of a terminal + fn render_terminal( + &mut self, + fc: &Vec, + y: usize, + length: usize, + height: usize, + ) -> Result { + if let Some(FileLayout::Terminal(term)) = self.files.get_raw(fc.to_owned()) { + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); + let editor_bg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); + let shift_down = term + .output + .matches('\n') + .count() + .saturating_sub(height.saturating_sub(1)); + // Calculate the contents and amount of padding for this line of the terminal + let mut lines = term.output.split('\n').skip(shift_down); + let (line, pad) = if let Some(line) = lines.nth(y) { + // Calculate line and padding + let line = line.replace("\r", "").replace("\n", ""); + let visible_line = strip_escape_codes(&line); + let w = width(&remove_ansi_codes(&line), 4); + // Work out if this is where the cursor should be + if lines.nth(y + 1).is_none() && self.ptr == *fc { + self.render_cache.term_cursor = Some(Loc { x: w, y }); + } + // Return the result + (visible_line, length.saturating_sub(w)) + } else { + (" ".repeat(length), 0) + }; + // Calculate the final result + Ok(format!("{editor_fg}{editor_bg}{line}{}", " ".repeat(pad))) + } else { + unreachable!() + } + } + /// Display a prompt in the document pub fn prompt>(&mut self, prompt: S) -> Result { let prompt = prompt.into(); diff --git a/src/pty.rs b/src/pty.rs index 111e6ec0..779fb171 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -63,7 +63,7 @@ impl Pty { } // Read the output let mut reader = BufReader::new(stream); - let mut buf = [0u8; 1024]; + let mut buf = [0u8; 10240]; let bytes_read = reader.read(&mut buf)?; let mut output = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); // Add on the output diff --git a/src/ui.rs b/src/ui.rs index e54bca54..4ae2f222 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -22,6 +22,7 @@ use mlua::AnyUserData; use std::collections::HashMap; use std::env; use std::io::{stdout, Stdout, Write}; +use synoptic::Regex; /// Printing macro #[macro_export] @@ -339,3 +340,59 @@ pub fn get_xterm_lookup() -> HashMap { } result } + +/// Remove ANSI codes from a string +pub fn remove_ansi_codes(input: &str) -> String { + // Define a regular expression to match ANSI escape codes and other control sequences + let invisible_regex = + Regex::new(r"[\x00-\x1F\x7F-\x9F]|\x1b(?:[@-_]|\[[0-9;?]*[a-zA-Z])|\[[0-9;?]*[a-zA-Z]") + .unwrap(); + // Replace all matches with an empty string + invisible_regex.replace_all(input, "").to_string() +} + +/// Remove all ANSI codes outside of color and attribute codes +pub fn strip_escape_codes(input: &str) -> String { + // Define a regular expression to match all escape sequences + let re = Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]").unwrap(); + + // Replace escape sequences, keeping those for attributes and colors + re.replace_all(input, |caps: ®ex::Captures| { + let code = caps.get(0).unwrap().as_str(); + + // Check if the escape code is for an allowed attribute or color + if code.contains("1m") // Bold + || code.contains("4m") // Underline + || code.contains("38;5") // Foreground color (256-color mode) + || code.contains("48;5") // Background color (256-color mode) + || code.contains("38;2") // Foreground color (true color) + || code.contains("48;2") + // Background color (true color) + { + // Return the escape code unchanged + code.to_string() + } else { + // Return an empty string for non-allowed escape codes (including cursor styling) + "".to_string() + } + }) + .to_string() +} + +/* +/// Replace reset background ANSI codes with a custom background color. +pub fn replace_reset_background(input: &str, custom_bg: &str) -> String { + // Define the regex to match reset background ANSI codes + let reset_bg_regex = Regex::new(r"\x1b\[49m|\x1b\[0m").unwrap(); + // Replace reset background with the custom background color + reset_bg_regex.replace_all(input, custom_bg).to_string() +} + +/// Replace reset foreground ANSI codes with a custom foreground color. +pub fn replace_reset_foreground(input: &str, custom_fg: &str) -> String { + // Define the regex to match reset foreground ANSI codes + let reset_fg_regex = Regex::new(r"\x1b\[39m|\x1b\[0m").unwrap(); + // Replace reset foreground with the custom foreground color + reset_fg_regex.replace_all(input, custom_fg).to_string() +} +*/ From 30b0ec4b15f91bf25c627c9d6106cfe920e9c261 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:29:05 +0000 Subject: [PATCH 06/22] clippy --- src/config/editor.rs | 2 +- src/editor/interface.rs | 3 ++- src/pty.rs | 2 +- src/ui.rs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index f3fc9f14..3f206a66 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -817,7 +817,7 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_right", |_, editor, ()| { - if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Ok(term) = Pty::new(Shell::Bash) { editor.ptr = editor .files .open_right(editor.ptr.clone(), FileLayout::Terminal(term)); diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 0ea595b2..1c1e7d1b 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -636,6 +636,7 @@ impl Editor { } /// Render the line of a terminal + #[allow(clippy::similar_names)] fn render_terminal( &mut self, fc: &Vec, @@ -655,7 +656,7 @@ impl Editor { let mut lines = term.output.split('\n').skip(shift_down); let (line, pad) = if let Some(line) = lines.nth(y) { // Calculate line and padding - let line = line.replace("\r", "").replace("\n", ""); + let line = line.replace(['\n', '\r'], ""); let visible_line = strip_escape_codes(&line); let w = width(&remove_ansi_codes(&line), 4); // Work out if this is where the cursor should be diff --git a/src/pty.rs b/src/pty.rs index 779fb171..b936e35c 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -59,7 +59,7 @@ impl Pty { std::thread::sleep(std::time::Duration::from_millis(100)); if self.shell.manual_input_echo() { // println!("Adding (pre-cmd) {:?}", cmd); - self.output += &cmd; + self.output += cmd; } // Read the output let mut reader = BufReader::new(stream); diff --git a/src/ui.rs b/src/ui.rs index 4ae2f222..a58a3c85 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -373,7 +373,7 @@ pub fn strip_escape_codes(input: &str) -> String { code.to_string() } else { // Return an empty string for non-allowed escape codes (including cursor styling) - "".to_string() + String::new() } }) .to_string() From daac4d3240c31fbc60ab6c0e08a1b02d97042016 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:49:37 +0000 Subject: [PATCH 07/22] Added more terminal interaction --- src/editor/interface.rs | 27 ++++------ src/editor/mod.rs | 107 +++++++++++++++++++++++++--------------- src/pty.rs | 4 ++ 3 files changed, 80 insertions(+), 58 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 1c1e7d1b..665b986e 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -637,36 +637,29 @@ impl Editor { /// Render the line of a terminal #[allow(clippy::similar_names)] - fn render_terminal( - &mut self, - fc: &Vec, - y: usize, - length: usize, - height: usize, - ) -> Result { + fn render_terminal(&mut self, fc: &Vec, y: usize, l: usize, h: usize) -> Result { if let Some(FileLayout::Terminal(term)) = self.files.get_raw(fc.to_owned()) { let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); let editor_bg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); - let shift_down = term - .output - .matches('\n') - .count() - .saturating_sub(height.saturating_sub(1)); + let n_lines = term.output.matches('\n').count(); + let shift_down = n_lines.saturating_sub(h.saturating_sub(1)); // Calculate the contents and amount of padding for this line of the terminal let mut lines = term.output.split('\n').skip(shift_down); let (line, pad) = if let Some(line) = lines.nth(y) { // Calculate line and padding let line = line.replace(['\n', '\r'], ""); - let visible_line = strip_escape_codes(&line); - let w = width(&remove_ansi_codes(&line), 4); + let mut visible_line = strip_escape_codes(&line); + let mut w = width(&remove_ansi_codes(&line), 4); // Work out if this is where the cursor should be - if lines.nth(y + 1).is_none() && self.ptr == *fc { + if n_lines.saturating_sub(shift_down) == y && self.ptr == *fc { + visible_line += &term.input; + w += width(&term.input, 4); self.render_cache.term_cursor = Some(Loc { x: w, y }); } // Return the result - (visible_line, length.saturating_sub(w)) + (visible_line, l.saturating_sub(w)) } else { - (" ".repeat(length), 0) + (" ".repeat(l), 0) }; // Calculate the final result Ok(format!("{editor_fg}{editor_bg}{line}{}", " ".repeat(pad))) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 7a733cc8..159f4374 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -332,29 +332,42 @@ impl Editor { /// Quit the editor pub fn quit(&mut self) -> Result<()> { - // Get the atom we're currently at - if let Some((fcs, ptr)) = self.files.get_atom(self.ptr.clone()) { - let last_file = fcs.len() == 1; - // Remove the file that is currently open and selected - let msg = "This document isn't saved, press Ctrl + Q to force quit or Esc to cancel"; - let doc = &fcs[ptr].doc; - if doc.event_mgmt.with_disk(&doc.take_snapshot()) || self.confirm(msg)? { - let (fcs, ptr) = self.files.get_atom_mut(self.ptr.clone()).unwrap(); - fcs.remove(*ptr); - self.prev(); + match self.files.get_raw(self.ptr.clone()) { + Some(FileLayout::Atom(fcs, ptr)) => { + let last_file = fcs.len() == 1; + // Remove the file that is currently open and selected + let msg = + "This document isn't saved, press Ctrl + Q to force quit or Esc to cancel"; + let doc = &fcs[*ptr].doc; + if doc.event_mgmt.with_disk(&doc.take_snapshot()) || self.confirm(msg)? { + let (fcs, ptr) = self.files.get_atom_mut(self.ptr.clone()).unwrap(); + fcs.remove(*ptr); + self.prev(); + } + // Perform cleanup / pointer reassignment if this atom is now empty + if last_file { + // Clean up the file structure + self.files.clean_up(); + // Find a new pointer position + self.ptr = self.files.new_pointer_position(&self.ptr); + // Clean up the redundant sidebyside/toptobottom + self.ptr = self.files.clean_up_multis(self.ptr.clone()); + } } - // Perform cleanup / pointer reassignment if this atom is now empty - if last_file { - // Clean up the file structure - self.files.clean_up(); + Some(FileLayout::Terminal(_)) => { + self.files.remove(self.ptr.clone()); // Find a new pointer position self.ptr = self.files.new_pointer_position(&self.ptr); // Clean up the redundant sidebyside/toptobottom self.ptr = self.files.clean_up_multis(self.ptr.clone()); } + _ => (), } // If there are no longer any active atoms, quit the entire editor - self.active = !matches!(self.files, FileLayout::None | FileLayout::FileTree); + self.active = !matches!( + self.files, + FileLayout::None | FileLayout::FileTree | FileLayout::Terminal(_) + ); Ok(()) } @@ -456,13 +469,9 @@ impl Editor { /// Handle key event pub fn handle_key_event(&mut self, modifiers: KMod, code: KCode) -> Result<()> { - let in_file_tree = matches!( - self.files.get_raw(self.ptr.clone()), - Some(FileLayout::FileTree) - ); - if in_file_tree { + match self.files.get_raw_mut(self.ptr.clone()) { // File tree key behaviour - match (modifiers, code) { + Some(FileLayout::FileTree) => match (modifiers, code) { (KMod::NONE, KCode::Up) => self.file_tree_select_up(), (KMod::NONE, KCode::Down) => self.file_tree_select_down(), (KMod::NONE, KCode::Enter) => self.file_tree_open_node()?, @@ -474,27 +483,43 @@ impl Editor { (KMod::NONE, KCode::Char('m')) => self.file_tree_move()?, (KMod::NONE, KCode::Char('c')) => self.file_tree_copy()?, _ => (), + }, + // Terminal behaviour + Some(FileLayout::Terminal(term)) => { + match (modifiers, code) { + (KMod::NONE, KCode::Enter) => term.char_input('\n')?, + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.char_input(ch)?, + (KMod::NONE, KCode::Backspace) => term.char_pop(), + (KMod::CONTROL, KCode::Char('l')) => { + // Clear the terminal + term.output.clear(); + term.run_command("\n")?; + term.output = term.output.trim_start_matches('\n').to_string(); + } + _ => (), + } } - } else { - // Non file tree behaviour - // Check period of inactivity - let end = Instant::now(); - let inactivity = end.duration_since(self.last_active).as_millis() as usize; - // Commit if over user-defined period of inactivity - if inactivity > config!(self.config, document).undo_period * 1000 { - self.try_doc_mut().unwrap().commit(); - } - // Register this activity - self.last_active = Instant::now(); - // Editing - these key bindings can't be modified (only added to)! - match (modifiers, code) { - // Core key bindings (non-configurable behaviour) - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, - (KMod::NONE, KCode::Tab) => self.handle_tab()?, - (KMod::NONE, KCode::Backspace) => self.backspace()?, - (KMod::NONE, KCode::Delete) => self.delete()?, - (KMod::NONE, KCode::Enter) => self.enter()?, - _ => (), + // File behaviour + _ => { + // Check period of inactivity + let end = Instant::now(); + let inactivity = end.duration_since(self.last_active).as_millis() as usize; + // Commit if over user-defined period of inactivity + if inactivity > config!(self.config, document).undo_period * 1000 { + self.try_doc_mut().unwrap().commit(); + } + // Register this activity + self.last_active = Instant::now(); + // Editing - these key bindings can't be modified (only added to)! + match (modifiers, code) { + // Core key bindings (non-configurable behaviour) + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, + (KMod::NONE, KCode::Tab) => self.handle_tab()?, + (KMod::NONE, KCode::Backspace) => self.backspace()?, + (KMod::NONE, KCode::Delete) => self.delete()?, + (KMod::NONE, KCode::Enter) => self.enter()?, + _ => (), + } } } Ok(()) diff --git a/src/pty.rs b/src/pty.rs index b936e35c..30136fb8 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -84,4 +84,8 @@ impl Pty { } Ok(()) } + + pub fn char_pop(&mut self) { + self.input.pop(); + } } From 178c724036b021ca54bf35929284e2f4f2c66f86 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:59:16 +0000 Subject: [PATCH 08/22] Stopped plug-ins and config file from activating in non-atoms --- src/editor/mod.rs | 4 +++ src/main.rs | 72 ++++++++++++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 159f4374..a91b2afd 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -496,6 +496,10 @@ impl Editor { term.run_command("\n")?; term.output = term.output.trim_start_matches('\n').to_string(); } + (KMod::CONTROL, KCode::Char('q')) => { + // Exit the terminal + self.quit()?; + } _ => (), } } diff --git a/src/main.rs b/src/main.rs index 7e8878be..1e605df8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -226,23 +226,29 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { ged!(mut &editor).feedback = Feedback::None; } - // Handle plug-in before key press mappings - if let CEvent::Key(key) = event { - let key_str = key_to_string(key.modifiers, key.code); - let code = run_key_before(&key_str); - let result = lua.load(&code).exec(); - handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); - } + // Only access plug-ins in atoms + let ptr = ged!(&editor).ptr.clone(); + let in_atom = ged!(&editor).files.get_atom(ptr).is_some(); + + if in_atom { + // Handle plug-in before key press mappings + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key_before(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); + } - // Handle paste event (before event) - if let CEvent::Paste(ref paste_text) = event { - let listeners = get_listeners("before:paste", lua)?; - for listener in listeners { - handle_lua_error( - "paste", - listener.call(paste_text.clone()), - &mut ged!(mut &editor).feedback, - ); + // Handle paste event (before event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("before:paste", lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut ged!(mut &editor).feedback, + ); + } } } @@ -262,24 +268,26 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { } } - // Handle paste event (after event) - if let CEvent::Paste(ref paste_text) = event { - let listeners = get_listeners("paste", lua)?; - for listener in listeners { - handle_lua_error( - "paste", - listener.call(paste_text.clone()), - &mut ged!(mut &editor).feedback, - ); + if in_atom { + // Handle paste event (after event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("paste", lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut ged!(mut &editor).feedback, + ); + } } - } - // Handle plug-in after key press mappings (if no errors occured) - if let CEvent::Key(key) = event { - let key_str = key_to_string(key.modifiers, key.code); - let code = run_key(&key_str); - let result = lua.load(&code).exec(); - handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); + // Handle plug-in after key press mappings (if no errors occured) + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); + } } Ok(()) From 6e8bf22f47bea93c5e35598b23504e5e8185225a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:14:12 +0000 Subject: [PATCH 09/22] Fixed issues with terminal rendering --- src/editor/interface.rs | 12 +++++++++--- src/main.rs | 13 ++++++++----- src/ui.rs | 2 -- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 665b986e..a16b125c 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -3,7 +3,10 @@ use crate::config::SyntaxHighlighting as SH; use crate::editor::{FTParts, FileLayout}; use crate::error::{OxError, Result}; use crate::events::wait_for_event_hog; -use crate::ui::{key_event, remove_ansi_codes, size, strip_escape_codes, Feedback}; +use crate::ui::{ + key_event, remove_ansi_codes, replace_reset_background, replace_reset_foreground, size, + strip_escape_codes, Feedback, +}; use crate::{config, display, handle_lua_error}; use crossterm::{ event::{KeyCode as KCode, KeyModifiers as KMod}, @@ -123,7 +126,7 @@ impl Editor { result += &self.render_file_tree(y, length)?; } else if in_terminal { // Part of terminal! - result += &self.render_terminal(fc, y, length, height)?; + result += &self.render_terminal(fc, rel_y, length, height)?; } else if y == rows.start && tab_line_enabled { // Tab line result += &self.render_tab_line(fc, lua, length)?; @@ -640,7 +643,7 @@ impl Editor { fn render_terminal(&mut self, fc: &Vec, y: usize, l: usize, h: usize) -> Result { if let Some(FileLayout::Terminal(term)) = self.files.get_raw(fc.to_owned()) { let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); - let editor_bg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?).to_string(); let n_lines = term.output.matches('\n').count(); let shift_down = n_lines.saturating_sub(h.saturating_sub(1)); // Calculate the contents and amount of padding for this line of the terminal @@ -649,6 +652,9 @@ impl Editor { // Calculate line and padding let line = line.replace(['\n', '\r'], ""); let mut visible_line = strip_escape_codes(&line); + // Replace resets with editor style + visible_line = replace_reset_background(&visible_line, &editor_bg); + visible_line = replace_reset_foreground(&visible_line, &editor_fg); let mut w = width(&remove_ansi_codes(&line), 4); // Work out if this is where the cursor should be if n_lines.saturating_sub(shift_down) == y && self.ptr == *fc { diff --git a/src/main.rs b/src/main.rs index 1e605df8..077b841e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use config::{ PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN, }; use crossterm::event::{Event as CEvent, KeyEvent, KeyEventKind}; -use editor::{allowed_by_multi_cursor, handle_multiple_cursors, Editor, FileTypes}; +use editor::{allowed_by_multi_cursor, handle_multiple_cursors, Editor, FileLayout, FileTypes}; use error::{OxError, Result}; use events::wait_for_event; use kaolinite::event::{Error as KError, Event}; @@ -226,11 +226,14 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { ged!(mut &editor).feedback = Feedback::None; } - // Only access plug-ins in atoms + // Only access plug-ins in certain layouts let ptr = ged!(&editor).ptr.clone(); - let in_atom = ged!(&editor).files.get_atom(ptr).is_some(); + let feed_in_plugins = matches!( + ged!(&editor).files.get_raw(ptr), + Some(FileLayout::FileTree | FileLayout::Atom(_, _)) + ); - if in_atom { + if feed_in_plugins { // Handle plug-in before key press mappings if let CEvent::Key(key) = event { let key_str = key_to_string(key.modifiers, key.code); @@ -268,7 +271,7 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { } } - if in_atom { + if feed_in_plugins { // Handle paste event (after event) if let CEvent::Paste(ref paste_text) = event { let listeners = get_listeners("paste", lua)?; diff --git a/src/ui.rs b/src/ui.rs index a58a3c85..bffbe862 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -379,7 +379,6 @@ pub fn strip_escape_codes(input: &str) -> String { .to_string() } -/* /// Replace reset background ANSI codes with a custom background color. pub fn replace_reset_background(input: &str, custom_bg: &str) -> String { // Define the regex to match reset background ANSI codes @@ -395,4 +394,3 @@ pub fn replace_reset_foreground(input: &str, custom_fg: &str) -> String { // Replace reset foreground with the custom foreground color reset_fg_regex.replace_all(input, custom_fg).to_string() } -*/ From dd119cb4ed038cea449823dc5060ae95e1ebf9b6 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:20:18 +0000 Subject: [PATCH 10/22] Reverted terminal prevention of config key bindings --- src/editor/mod.rs | 4 --- src/main.rs | 75 ++++++++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 47 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index a91b2afd..159f4374 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -496,10 +496,6 @@ impl Editor { term.run_command("\n")?; term.output = term.output.trim_start_matches('\n').to_string(); } - (KMod::CONTROL, KCode::Char('q')) => { - // Exit the terminal - self.quit()?; - } _ => (), } } diff --git a/src/main.rs b/src/main.rs index 077b841e..1f9161ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -226,32 +226,23 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { ged!(mut &editor).feedback = Feedback::None; } - // Only access plug-ins in certain layouts - let ptr = ged!(&editor).ptr.clone(); - let feed_in_plugins = matches!( - ged!(&editor).files.get_raw(ptr), - Some(FileLayout::FileTree | FileLayout::Atom(_, _)) - ); - - if feed_in_plugins { - // Handle plug-in before key press mappings - if let CEvent::Key(key) = event { - let key_str = key_to_string(key.modifiers, key.code); - let code = run_key_before(&key_str); - let result = lua.load(&code).exec(); - handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); - } + // Handle plug-in before key press mappings + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key_before(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); + } - // Handle paste event (before event) - if let CEvent::Paste(ref paste_text) = event { - let listeners = get_listeners("before:paste", lua)?; - for listener in listeners { - handle_lua_error( - "paste", - listener.call(paste_text.clone()), - &mut ged!(mut &editor).feedback, - ); - } + // Handle paste event (before event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("before:paste", lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut ged!(mut &editor).feedback, + ); } } @@ -271,26 +262,24 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { } } - if feed_in_plugins { - // Handle paste event (after event) - if let CEvent::Paste(ref paste_text) = event { - let listeners = get_listeners("paste", lua)?; - for listener in listeners { - handle_lua_error( - "paste", - listener.call(paste_text.clone()), - &mut ged!(mut &editor).feedback, - ); - } + // Handle paste event (after event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("paste", lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut ged!(mut &editor).feedback, + ); } + } - // Handle plug-in after key press mappings (if no errors occured) - if let CEvent::Key(key) = event { - let key_str = key_to_string(key.modifiers, key.code); - let code = run_key(&key_str); - let result = lua.load(&code).exec(); - handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); - } + // Handle plug-in after key press mappings (if no errors occured) + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); } Ok(()) From 8a3420c68872aa0eae4cffef6251e4154c939cff Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:39:30 +0000 Subject: [PATCH 11/22] Pre-loaded commands in the terminal opening api --- config/.oxrc | 16 ++++++++-------- src/config/editor.rs | 28 ++++++++++++++++++++-------- src/editor/mod.rs | 7 +------ src/main.rs | 2 +- src/pty.rs | 16 ++++++++++++++++ 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index f5c12039..0647f084 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -329,26 +329,26 @@ commands = { local file = arguments[2] local result = false if arguments[1] == "left" then - if arguments[2] == "terminal" then - result = editor:open_terminal_left() + if arguments[2] == "terminal" or arguments[2] == "term" then + result = editor:open_terminal_left(table.concat(arguments, " ", 3)) else result = editor:open_split_left(file) end elseif arguments[1] == "right" then - if arguments[2] == "terminal" then - result = editor:open_terminal_right() + if arguments[2] == "terminal" or arguments[2] == "term" then + result = editor:open_terminal_right(table.concat(arguments, " ", 3)) else result = editor:open_split_right(file) end elseif arguments[1] == "up" then - if arguments[2] == "terminal" then - result = editor:open_terminal_up() + if arguments[2] == "terminal" or arguments[2] == "term" then + result = editor:open_terminal_up(table.concat(arguments, " ", 3)) else result = editor:open_split_up(file) end elseif arguments[1] == "down" then - if arguments[2] == "terminal" then - result = editor:open_terminal_down() + if arguments[2] == "terminal" or arguments[2] == "term" then + result = editor:open_terminal_down(table.concat(arguments, " ", 3)) else result = editor:open_split_down(file) end diff --git a/src/config/editor.rs b/src/config/editor.rs index 3f206a66..e9f14e76 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -783,8 +783,11 @@ impl LuaUserData for Editor { Ok(()) }); // Terminal - methods.add_method_mut("open_terminal_up", |_, editor, ()| { - if let Ok(term) = Pty::new(Shell::Bash) { + methods.add_method_mut("open_terminal_up", |_, editor, cmd: Option| { + if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Some(cmd) = cmd { + term.silent_run_command(&format!("{cmd}\n"))?; + } editor.ptr = editor .files .open_up(editor.ptr.clone(), FileLayout::Terminal(term)); @@ -794,8 +797,11 @@ impl LuaUserData for Editor { Ok(false) } }); - methods.add_method_mut("open_terminal_down", |_, editor, ()| { - if let Ok(term) = Pty::new(Shell::Bash) { + methods.add_method_mut("open_terminal_down", |_, editor, cmd: Option| { + if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Some(cmd) = cmd { + term.silent_run_command(&format!("{cmd}\n"))?; + } editor.ptr = editor .files .open_down(editor.ptr.clone(), FileLayout::Terminal(term)); @@ -805,8 +811,11 @@ impl LuaUserData for Editor { Ok(false) } }); - methods.add_method_mut("open_terminal_left", |_, editor, ()| { - if let Ok(term) = Pty::new(Shell::Bash) { + methods.add_method_mut("open_terminal_left", |_, editor, cmd: Option| { + if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Some(cmd) = cmd { + term.silent_run_command(&format!("{cmd}\n"))?; + } editor.ptr = editor .files .open_left(editor.ptr.clone(), FileLayout::Terminal(term)); @@ -816,8 +825,11 @@ impl LuaUserData for Editor { Ok(false) } }); - methods.add_method_mut("open_terminal_right", |_, editor, ()| { - if let Ok(term) = Pty::new(Shell::Bash) { + methods.add_method_mut("open_terminal_right", |_, editor, cmd: Option| { + if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Some(cmd) = cmd { + term.silent_run_command(&format!("{cmd}\n"))?; + } editor.ptr = editor .files .open_right(editor.ptr.clone(), FileLayout::Terminal(term)); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 159f4374..f9cdcd89 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -490,12 +490,7 @@ impl Editor { (KMod::NONE, KCode::Enter) => term.char_input('\n')?, (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.char_input(ch)?, (KMod::NONE, KCode::Backspace) => term.char_pop(), - (KMod::CONTROL, KCode::Char('l')) => { - // Clear the terminal - term.output.clear(); - term.run_command("\n")?; - term.output = term.output.trim_start_matches('\n').to_string(); - } + (KMod::CONTROL, KCode::Char('l')) => term.clear()?, _ => (), } } diff --git a/src/main.rs b/src/main.rs index 1f9161ea..7e8878be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use config::{ PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN, }; use crossterm::event::{Event as CEvent, KeyEvent, KeyEventKind}; -use editor::{allowed_by_multi_cursor, handle_multiple_cursors, Editor, FileLayout, FileTypes}; +use editor::{allowed_by_multi_cursor, handle_multiple_cursors, Editor, FileTypes}; use error::{OxError, Result}; use events::wait_for_event; use kaolinite::event::{Error as KError, Event}; diff --git a/src/pty.rs b/src/pty.rs index 30136fb8..f7df3ac7 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -75,6 +75,15 @@ impl Pty { Ok(()) } + pub fn silent_run_command(&mut self, cmd: &str) -> Result<()> { + self.output.clear(); + self.run_command(cmd)?; + if self.output.starts_with(cmd) { + self.output = self.output.chars().skip(cmd.chars().count()).collect(); + } + Ok(()) + } + pub fn char_input(&mut self, c: char) -> Result<()> { self.input.push(c); if c == '\n' { @@ -88,4 +97,11 @@ impl Pty { pub fn char_pop(&mut self) { self.input.pop(); } + + pub fn clear(&mut self) -> Result<()> { + self.output.clear(); + self.run_command("\n")?; + self.output = self.output.trim_start_matches('\n').to_string(); + Ok(()) + } } From 763e18eb7533e10f3b75d63c586800bd2531f864 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:39:57 +0000 Subject: [PATCH 12/22] rustfmt --- src/editor/mod.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index f9cdcd89..510b921d 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -485,15 +485,13 @@ impl Editor { _ => (), }, // Terminal behaviour - Some(FileLayout::Terminal(term)) => { - match (modifiers, code) { - (KMod::NONE, KCode::Enter) => term.char_input('\n')?, - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.char_input(ch)?, - (KMod::NONE, KCode::Backspace) => term.char_pop(), - (KMod::CONTROL, KCode::Char('l')) => term.clear()?, - _ => (), - } - } + Some(FileLayout::Terminal(term)) => match (modifiers, code) { + (KMod::NONE, KCode::Enter) => term.char_input('\n')?, + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.char_input(ch)?, + (KMod::NONE, KCode::Backspace) => term.char_pop(), + (KMod::CONTROL, KCode::Char('l')) => term.clear()?, + _ => (), + }, // File behaviour _ => { // Check period of inactivity From e25d3d8dc6fcba43edf784627a52ce969fe4badf Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 16:09:03 +0000 Subject: [PATCH 13/22] configurable shells --- config/.oxrc | 3 +++ src/config/editor.rs | 10 ++++---- src/config/interface.rs | 8 ++++++ src/editor/interface.rs | 8 ++++-- src/pty.rs | 33 +++++++++++++++++++++--- src/ui.rs | 56 ++++++++++++++++++++++++++++------------- 6 files changed, 90 insertions(+), 28 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 0647f084..8b3ebeb1 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -459,6 +459,9 @@ line_numbers.padding_right = 1 terminal.mouse_enabled = true terminal.scroll_amount = 4 +-- Configure Terminal Behaviour -- +terminal.shell = "bash" + -- Configure File Tree -- file_tree.width = 30 file_tree.move_focus_to_file = true diff --git a/src/config/editor.rs b/src/config/editor.rs index e9f14e76..2aec0a96 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -1,7 +1,7 @@ /// Defines the Editor API for plug-ins to use use crate::cli::VERSION; use crate::editor::{Editor, FileContainer, FileLayout}; -use crate::pty::{Pty, Shell}; +use crate::pty::Pty; use crate::ui::Feedback; use crate::{config, fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; @@ -784,7 +784,7 @@ impl LuaUserData for Editor { }); // Terminal methods.add_method_mut("open_terminal_up", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { term.silent_run_command(&format!("{cmd}\n"))?; } @@ -798,7 +798,7 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_down", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { term.silent_run_command(&format!("{cmd}\n"))?; } @@ -812,7 +812,7 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_left", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { term.silent_run_command(&format!("{cmd}\n"))?; } @@ -826,7 +826,7 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_right", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(Shell::Bash) { + if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { term.silent_run_command(&format!("{cmd}\n"))?; } diff --git a/src/config/interface.rs b/src/config/interface.rs index 303961f2..397fd918 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -1,6 +1,7 @@ /// Utilities for configuring and rendering parts of the interface use crate::cli::VERSION; use crate::editor::{Editor, FileContainer}; +use crate::pty::Shell; use crate::Feedback; use kaolinite::searching::Searcher; use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; @@ -16,6 +17,7 @@ type LuaRes = RResult; pub struct Terminal { pub mouse_enabled: bool, pub scroll_amount: usize, + pub shell: Shell, } impl Default for Terminal { @@ -23,6 +25,7 @@ impl Default for Terminal { Self { mouse_enabled: true, scroll_amount: 1, + shell: Shell::Bash, } } } @@ -39,6 +42,11 @@ impl LuaUserData for Terminal { this.scroll_amount = value; Ok(()) }); + fields.add_field_method_get("shell", |_, this| Ok(this.shell)); + fields.add_field_method_set("shell", |_, this, value| { + this.shell = value; + Ok(()) + }); } } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index a16b125c..194209c9 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -644,6 +644,7 @@ impl Editor { if let Some(FileLayout::Terminal(term)) = self.files.get_raw(fc.to_owned()) { let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?).to_string(); + let reset = SetAttribute(Attribute::NoBold); let n_lines = term.output.matches('\n').count(); let shift_down = n_lines.saturating_sub(h.saturating_sub(1)); // Calculate the contents and amount of padding for this line of the terminal @@ -658,7 +659,7 @@ impl Editor { let mut w = width(&remove_ansi_codes(&line), 4); // Work out if this is where the cursor should be if n_lines.saturating_sub(shift_down) == y && self.ptr == *fc { - visible_line += &term.input; + visible_line += &format!("{editor_fg}{reset}{}", term.input); w += width(&term.input, 4); self.render_cache.term_cursor = Some(Loc { x: w, y }); } @@ -668,7 +669,10 @@ impl Editor { (" ".repeat(l), 0) }; // Calculate the final result - Ok(format!("{editor_fg}{editor_bg}{line}{}", " ".repeat(pad))) + Ok(format!( + "{reset}{editor_fg}{editor_bg}{line}{}", + " ".repeat(pad) + )) } else { unreachable!() } diff --git a/src/pty.rs b/src/pty.rs index f7df3ac7..2eabf443 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -1,3 +1,4 @@ +use mlua::prelude::*; /// User friendly interface for dealing with pseudo terminals use ptyprocess::PtyProcess; use std::io::{BufReader, Read, Result, Write}; @@ -11,7 +12,7 @@ pub struct Pty { pub shell: Shell, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Shell { Bash, Dash, @@ -20,11 +21,11 @@ pub enum Shell { } impl Shell { - pub fn manual_input_echo(&self) -> bool { + pub fn manual_input_echo(self) -> bool { matches!(self, Self::Bash | Self::Dash) } - pub fn inserts_extra_newline(&self) -> bool { + pub fn inserts_extra_newline(self) -> bool { !matches!(self, Self::Zsh) } @@ -38,6 +39,32 @@ impl Shell { } } +impl IntoLua for Shell { + fn into_lua(self, lua: &Lua) -> LuaResult { + let string = lua.create_string(self.command())?; + Ok(LuaValue::String(string)) + } +} + +impl FromLua for Shell { + fn from_lua(val: LuaValue, _: &Lua) -> LuaResult { + Ok(if let LuaValue::String(inner) = val { + if let Ok(s) = inner.to_str() { + match s.to_owned().as_str() { + "dash" => Self::Dash, + "zsh" => Self::Zsh, + "fish" => Self::Fish, + _ => Self::Bash, + } + } else { + Self::Bash + } + } else { + Self::Bash + }) + } +} + impl Pty { pub fn new(shell: Shell) -> Result { let mut pty = Self { diff --git a/src/ui.rs b/src/ui.rs index bffbe862..92602671 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -345,38 +345,58 @@ pub fn get_xterm_lookup() -> HashMap { pub fn remove_ansi_codes(input: &str) -> String { // Define a regular expression to match ANSI escape codes and other control sequences let invisible_regex = - Regex::new(r"[\x00-\x1F\x7F-\x9F]|\x1b(?:[@-_]|\[[0-9;?]*[a-zA-Z])|\[[0-9;?]*[a-zA-Z]") + // Regex::new(r"[\x00-\x1F\x7F-\x9F]|\x1b(?:[@-_]|\[[0-9;?]*[a-zA-Z])|\[[0-9;?]*[a-zA-Z]") + Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\([a-zA-Z]|\x1b\].*?(\x07|\x1b\\)|\x1b(:?>|=)") .unwrap(); + let weird_newline = Regex::new(r"⏎\s*⏎\s?").unwrap(); + let long_spaces = Regex::new(r"%(?:\x1b\[1m)?\s{5,}").unwrap(); // Replace all matches with an empty string - invisible_regex.replace_all(input, "").to_string() + let result = invisible_regex.replace_all(input, "").to_string(); + // Replace weird new line stuff + let result = weird_newline.replace_all(&result, "").to_string(); + // Replace long spaces + let result = long_spaces.replace_all(&result, "").to_string(); + // Return result + result } /// Remove all ANSI codes outside of color and attribute codes pub fn strip_escape_codes(input: &str) -> String { // Define a regular expression to match all escape sequences - let re = Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]").unwrap(); - + // let re = Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]").unwrap(); + let re = + Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\([a-zA-Z]|\x1b\].*?(\x07|\x1b\\)|\x1b(:?>|=)") + .unwrap(); + let weird_newline = Regex::new(r"⏎\s*⏎\s?").unwrap(); + let long_spaces = Regex::new(r"%(?:\x1b\[1m)?\s{5,}").unwrap(); // Replace escape sequences, keeping those for attributes and colors - re.replace_all(input, |caps: ®ex::Captures| { - let code = caps.get(0).unwrap().as_str(); + let result = re + .replace_all(input, |caps: ®ex::Captures| { + let code = caps.get(0).unwrap().as_str(); - // Check if the escape code is for an allowed attribute or color - if code.contains("1m") // Bold + // Check if the escape code is for an allowed attribute or color + if code.contains("1m") // Bold || code.contains("4m") // Underline || code.contains("38;5") // Foreground color (256-color mode) || code.contains("48;5") // Background color (256-color mode) || code.contains("38;2") // Foreground color (true color) || code.contains("48;2") - // Background color (true color) - { - // Return the escape code unchanged - code.to_string() - } else { - // Return an empty string for non-allowed escape codes (including cursor styling) - String::new() - } - }) - .to_string() + // Background color (true color) + { + // Return the escape code unchanged + code.to_string() + } else { + // Return an empty string for non-allowed escape codes (including cursor styling) + String::new() + } + }) + .to_string(); + // Replace weird new line stuff + let result = weird_newline.replace_all(&result, "").to_string(); + // Replace long spaces + let result = long_spaces.replace_all(&result, "").to_string(); + // Return result + result } /// Replace reset background ANSI codes with a custom background color. From f584a0e46e29252e6462b0051b6a95ccee11cd82 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:04:53 +0000 Subject: [PATCH 14/22] write update thread --- Cargo.lock | 22 ++++++++++++- Cargo.toml | 2 ++ src/config/editor.rs | 16 +++++----- src/editor/documents.rs | 28 ++++++++++++++++- src/editor/interface.rs | 5 +-- src/editor/mod.rs | 14 +++++---- src/events.rs | 5 +++ src/pty.rs | 68 +++++++++++++++++++++++++++++++++++++---- 8 files changed, 136 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b01804a..f971396a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "char_index" version = "0.1.4" @@ -338,6 +344,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -369,7 +387,9 @@ dependencies = [ "error_set", "jargon-args", "kaolinite", + "mio", "mlua", + "nix 0.29.0", "ptyprocess", "regex", "shellexpand", @@ -435,7 +455,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e05aef7befb11a210468a2d77d978dde2c6381a0381e33beb575e91f57fe8cf" dependencies = [ - "nix", + "nix 0.26.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b115764c..0fc41369 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,5 @@ shellexpand = "3.1.0" synoptic = "2.2.9" ptyprocess = "0.4.1" regex = "1.11.1" +mio = { version = "1.0.3", features = ["os-ext"] } +nix = { version = "0.29.0", features = ["fs"] } diff --git a/src/config/editor.rs b/src/config/editor.rs index 2aec0a96..71a10403 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -784,9 +784,9 @@ impl LuaUserData for Editor { }); // Terminal methods.add_method_mut("open_terminal_up", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { + if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.silent_run_command(&format!("{cmd}\n"))?; + term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files @@ -798,9 +798,9 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_down", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { + if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.silent_run_command(&format!("{cmd}\n"))?; + term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files @@ -812,9 +812,9 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_left", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { + if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.silent_run_command(&format!("{cmd}\n"))?; + term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files @@ -826,9 +826,9 @@ impl LuaUserData for Editor { } }); methods.add_method_mut("open_terminal_right", |_, editor, cmd: Option| { - if let Ok(mut term) = Pty::new(config!(editor.config, terminal).shell) { + if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.silent_run_command(&format!("{cmd}\n"))?; + term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files diff --git a/src/editor/documents.rs b/src/editor/documents.rs index a9dfafbe..70ea70c7 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -6,6 +6,8 @@ use kaolinite::Document; use kaolinite::Size; use std::ops::Range; use synoptic::Highlighter; +use std::sync::{Arc, Mutex}; + pub type Span = Vec<(Vec, Range, Range)>; @@ -23,7 +25,7 @@ pub enum FileLayout { /// Representing a file tree FileTree, /// Representing a terminal - Terminal(Pty), + Terminal(Arc>), } impl Default for FileLayout { @@ -437,6 +439,30 @@ impl FileLayout { } } + /// Traverse the tree and return a list of indices to empty atoms + pub fn terminal_rerender(&mut self) -> bool { + match self { + Self::None | Self::FileTree | Self::Atom(_, _) => false, + Self::Terminal(term) => { + let mut term = term.lock().unwrap(); + if term.force_rerender { + term.force_rerender = false; + true + } else { + false + } + } + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { + for layout in layouts.iter_mut() { + if layout.0.terminal_rerender() { + return true; + } + } + false + } + } + } + /// Find a new pointer position when something is removed pub fn new_pointer_position(&self, old: &[usize]) -> Vec { // Zoom out until a sidebyside or toptobottom is found diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 194209c9..bef56ce2 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -642,14 +642,14 @@ impl Editor { #[allow(clippy::similar_names)] fn render_terminal(&mut self, fc: &Vec, y: usize, l: usize, h: usize) -> Result { if let Some(FileLayout::Terminal(term)) = self.files.get_raw(fc.to_owned()) { + let term = term.lock().unwrap(); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?).to_string(); let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?).to_string(); let reset = SetAttribute(Attribute::NoBold); let n_lines = term.output.matches('\n').count(); let shift_down = n_lines.saturating_sub(h.saturating_sub(1)); // Calculate the contents and amount of padding for this line of the terminal - let mut lines = term.output.split('\n').skip(shift_down); - let (line, pad) = if let Some(line) = lines.nth(y) { + let (line, pad) = if let Some(line) = term.output.split('\n').skip(shift_down).nth(y) { // Calculate line and padding let line = line.replace(['\n', '\r'], ""); let mut visible_line = strip_escape_codes(&line); @@ -668,6 +668,7 @@ impl Editor { } else { (" ".repeat(l), 0) }; + std::mem::drop(term); // Calculate the final result Ok(format!( "{reset}{editor_fg}{editor_bg}{line}{}", diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 510b921d..5d474fd2 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -485,12 +485,14 @@ impl Editor { _ => (), }, // Terminal behaviour - Some(FileLayout::Terminal(term)) => match (modifiers, code) { - (KMod::NONE, KCode::Enter) => term.char_input('\n')?, - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.char_input(ch)?, - (KMod::NONE, KCode::Backspace) => term.char_pop(), - (KMod::CONTROL, KCode::Char('l')) => term.clear()?, - _ => (), + Some(FileLayout::Terminal(term)) => { + match (modifiers, code) { + (KMod::NONE, KCode::Enter) => term.lock().unwrap().char_input('\n')?, + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.lock().unwrap().char_input(ch)?, + (KMod::NONE, KCode::Backspace) => term.lock().unwrap().char_pop(), + (KMod::CONTROL, KCode::Char('l')) => term.lock().unwrap().clear()?, + _ => (), + } }, // File behaviour _ => { diff --git a/src/events.rs b/src/events.rs index 59e27cb9..2396ee5b 100644 --- a/src/events.rs +++ b/src/events.rs @@ -24,6 +24,11 @@ pub fn wait_for_event(editor: &AnyUserData, lua: &Lua) -> Result { Feedback::Warning(format!("Function '{task}' was not found")); } } + // If a terminal dictates, force a rerender + if ged!(mut &editor).files.terminal_rerender() { + ged!(mut &editor).needs_rerender = true; + ged!(mut &editor).render(lua)?; + } } } diff --git a/src/pty.rs b/src/pty.rs index 2eabf443..681104e0 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -1,8 +1,15 @@ +//! User friendly interface for dealing with pseudo terminals + use mlua::prelude::*; -/// User friendly interface for dealing with pseudo terminals use ptyprocess::PtyProcess; use std::io::{BufReader, Read, Result, Write}; use std::process::Command; +use std::sync::{Arc, Mutex}; +use mio::{Events, Interest, Poll, Token}; +use mio::unix::SourceFd; +use std::os::unix::io::AsRawFd; +use std::time::Duration; +use nix::fcntl::{fcntl, FcntlArg, OFlag}; #[derive(Debug)] pub struct Pty { @@ -10,6 +17,7 @@ pub struct Pty { pub output: String, pub input: String, pub shell: Shell, + pub force_rerender: bool, } #[derive(Debug, Clone, Copy)] @@ -66,16 +74,28 @@ impl FromLua for Shell { } impl Pty { - pub fn new(shell: Shell) -> Result { - let mut pty = Self { + pub fn new(shell: Shell) -> Result>> { + let pty = Arc::new(Mutex::new(Self { process: PtyProcess::spawn(Command::new(shell.command()))?, output: String::new(), input: String::new(), shell, - }; - pty.process.set_echo(false, None)?; + force_rerender: false, + })); + pty.lock().unwrap().process.set_echo(false, None)?; std::thread::sleep(std::time::Duration::from_millis(100)); - pty.run_command("")?; + pty.lock().unwrap().run_command("")?; + // Spawn thread to constantly read from the terminal + let pty_clone = Arc::clone(&pty); + std::thread::spawn(move || { + loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + let mut pty = pty_clone.lock().unwrap(); + pty.force_rerender = matches!(pty.catch_up(), Ok(true)); + std::mem::drop(pty); + } + }); + // Return the pty Ok(pty) } @@ -131,4 +151,40 @@ impl Pty { self.output = self.output.trim_start_matches('\n').to_string(); Ok(()) } + + pub fn catch_up(&mut self) -> Result { + let stream = self.process.get_raw_handle()?; + let raw_fd = stream.as_raw_fd(); + let flags = fcntl(raw_fd, FcntlArg::F_GETFL).unwrap(); + fcntl(raw_fd, FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK)).unwrap(); + let mut source = SourceFd(&raw_fd); + // Set up mio Poll and register the raw_fd + let mut poll = Poll::new()?; + let mut events = Events::with_capacity(128); + poll.registry() + .register(&mut source, Token(0), Interest::READABLE)?; + match poll.poll(&mut events, Some(Duration::from_millis(100))) { + Ok(()) => { + // Data is available to read + let mut reader = BufReader::new(stream); + let mut buf = [0u8; 10240]; + let bytes_read = reader.read(&mut buf)?; + + // Process the read data + let mut output = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); + if self.shell.inserts_extra_newline() { + output = output.replace("\u{1b}[?2004l\r\r\n", ""); + } + + // Append the output to self.output + // println!("Adding (aftercmd) \"{:?}\"", output); + self.output += &output; + Ok(!output.is_empty()) + } + Err(e) => { + // Handle polling errors + return Err(e); + } + } + } } From ed3ae29e35cd9094ea92d1ac1bf534b72366bf61 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:05:05 +0000 Subject: [PATCH 15/22] rustfmt --- src/config/editor.rs | 16 ++++++++++++---- src/editor/documents.rs | 3 +-- src/editor/mod.rs | 14 +++++++------- src/pty.rs | 26 ++++++++++++++------------ 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 71a10403..8138a550 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -786,7 +786,9 @@ impl LuaUserData for Editor { methods.add_method_mut("open_terminal_up", |_, editor, cmd: Option| { if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; + term.lock() + .unwrap() + .silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files @@ -800,7 +802,9 @@ impl LuaUserData for Editor { methods.add_method_mut("open_terminal_down", |_, editor, cmd: Option| { if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; + term.lock() + .unwrap() + .silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files @@ -814,7 +818,9 @@ impl LuaUserData for Editor { methods.add_method_mut("open_terminal_left", |_, editor, cmd: Option| { if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; + term.lock() + .unwrap() + .silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files @@ -828,7 +834,9 @@ impl LuaUserData for Editor { methods.add_method_mut("open_terminal_right", |_, editor, cmd: Option| { if let Ok(term) = Pty::new(config!(editor.config, terminal).shell) { if let Some(cmd) = cmd { - term.lock().unwrap().silent_run_command(&format!("{cmd}\n"))?; + term.lock() + .unwrap() + .silent_run_command(&format!("{cmd}\n"))?; } editor.ptr = editor .files diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 70ea70c7..157cda9d 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -5,9 +5,8 @@ use crate::Loc; use kaolinite::Document; use kaolinite::Size; use std::ops::Range; -use synoptic::Highlighter; use std::sync::{Arc, Mutex}; - +use synoptic::Highlighter; pub type Span = Vec<(Vec, Range, Range)>; diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 5d474fd2..ef1711a0 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -485,14 +485,14 @@ impl Editor { _ => (), }, // Terminal behaviour - Some(FileLayout::Terminal(term)) => { - match (modifiers, code) { - (KMod::NONE, KCode::Enter) => term.lock().unwrap().char_input('\n')?, - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => term.lock().unwrap().char_input(ch)?, - (KMod::NONE, KCode::Backspace) => term.lock().unwrap().char_pop(), - (KMod::CONTROL, KCode::Char('l')) => term.lock().unwrap().clear()?, - _ => (), + Some(FileLayout::Terminal(term)) => match (modifiers, code) { + (KMod::NONE, KCode::Enter) => term.lock().unwrap().char_input('\n')?, + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => { + term.lock().unwrap().char_input(ch)? } + (KMod::NONE, KCode::Backspace) => term.lock().unwrap().char_pop(), + (KMod::CONTROL, KCode::Char('l')) => term.lock().unwrap().clear()?, + _ => (), }, // File behaviour _ => { diff --git a/src/pty.rs b/src/pty.rs index 681104e0..913c36df 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -1,15 +1,15 @@ //! User friendly interface for dealing with pseudo terminals +use mio::unix::SourceFd; +use mio::{Events, Interest, Poll, Token}; use mlua::prelude::*; +use nix::fcntl::{fcntl, FcntlArg, OFlag}; use ptyprocess::PtyProcess; use std::io::{BufReader, Read, Result, Write}; +use std::os::unix::io::AsRawFd; use std::process::Command; use std::sync::{Arc, Mutex}; -use mio::{Events, Interest, Poll, Token}; -use mio::unix::SourceFd; -use std::os::unix::io::AsRawFd; use std::time::Duration; -use nix::fcntl::{fcntl, FcntlArg, OFlag}; #[derive(Debug)] pub struct Pty { @@ -87,13 +87,11 @@ impl Pty { pty.lock().unwrap().run_command("")?; // Spawn thread to constantly read from the terminal let pty_clone = Arc::clone(&pty); - std::thread::spawn(move || { - loop { - std::thread::sleep(std::time::Duration::from_millis(100)); - let mut pty = pty_clone.lock().unwrap(); - pty.force_rerender = matches!(pty.catch_up(), Ok(true)); - std::mem::drop(pty); - } + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + let mut pty = pty_clone.lock().unwrap(); + pty.force_rerender = matches!(pty.catch_up(), Ok(true)); + std::mem::drop(pty); }); // Return the pty Ok(pty) @@ -156,7 +154,11 @@ impl Pty { let stream = self.process.get_raw_handle()?; let raw_fd = stream.as_raw_fd(); let flags = fcntl(raw_fd, FcntlArg::F_GETFL).unwrap(); - fcntl(raw_fd, FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK)).unwrap(); + fcntl( + raw_fd, + FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK), + ) + .unwrap(); let mut source = SourceFd(&raw_fd); // Set up mio Poll and register the raw_fd let mut poll = Poll::new()?; From fc7a4cd198f1581ed49bc30ba11fbaffc554e2af Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 00:36:05 +0000 Subject: [PATCH 16/22] added mouse support for terminal --- src/editor/interface.rs | 2 +- src/editor/mod.rs | 2 +- src/editor/mouse.rs | 104 ++++++++++++++++++++++------------------ src/pty.rs | 5 +- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index bef56ce2..a85290d4 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -649,7 +649,7 @@ impl Editor { let n_lines = term.output.matches('\n').count(); let shift_down = n_lines.saturating_sub(h.saturating_sub(1)); // Calculate the contents and amount of padding for this line of the terminal - let (line, pad) = if let Some(line) = term.output.split('\n').skip(shift_down).nth(y) { + let (line, pad) = if let Some(line) = term.output.split('\n').nth(shift_down + y) { // Calculate line and padding let line = line.replace(['\n', '\r'], ""); let mut visible_line = strip_escape_codes(&line); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index ef1711a0..27aabf82 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -488,7 +488,7 @@ impl Editor { Some(FileLayout::Terminal(term)) => match (modifiers, code) { (KMod::NONE, KCode::Enter) => term.lock().unwrap().char_input('\n')?, (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => { - term.lock().unwrap().char_input(ch)? + term.lock().unwrap().char_input(ch)?; } (KMod::NONE, KCode::Backspace) => term.lock().unwrap().char_pop(), (KMod::CONTROL, KCode::Char('l')) => term.lock().unwrap().clear()?, diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index c261eb64..5cfc4e7b 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -1,3 +1,4 @@ +use crate::editor::FileLayout; /// For handling mouse events use crate::{config, Result}; use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; @@ -15,6 +16,8 @@ enum MouseLocation { Tabs(Vec, usize), /// Where the mouse has clicked in the file tree FileTree(usize), + /// Where the mouse has clicked in the terminal + Terminal(Vec), /// Mouse has clicked nothing of importance Out, } @@ -36,53 +39,55 @@ impl Editor { if let Some((idx, rows, cols)) = at_idx { let idx = idx.clone(); // Calculate the current dent in this split - if let Some((_, doc_idx)) = self.files.get_atom(idx.clone()) { - let dent = self.dent_for(&idx, doc_idx); - // Split that user clicked in located - adjust event location - let clicked = Loc { - x: col.saturating_sub(cols.start), - y: row.saturating_sub(rows.start), - }; - // Work out where the user clicked - if clicked.y == 0 && tab_enabled { - // Clicked on tab line - let (tabs, _, offset) = - self.get_tab_parts(&idx, lua, cols.end.saturating_sub(cols.start)); - // Try to work out which tab we clicked on - let mut c = u16::try_from(clicked.x).unwrap_or(u16::MAX) + 2; - for (i, header) in tabs.iter().enumerate() { - let header_len = width(header, 4) + 1; - c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); - if c == 0 { - // This tab was clicked on - return MouseLocation::Tabs(idx.clone(), i + offset); + match self.files.get_raw(idx.clone()) { + Some(FileLayout::Atom(_, doc_idx)) => { + let dent = self.dent_for(&idx, *doc_idx); + // Split that user clicked in located - adjust event location + let clicked = Loc { + x: col.saturating_sub(cols.start), + y: row.saturating_sub(rows.start), + }; + // Work out where the user clicked + if clicked.y == 0 && tab_enabled { + // Clicked on tab line + let (tabs, _, offset) = + self.get_tab_parts(&idx, lua, cols.end.saturating_sub(cols.start)); + // Try to work out which tab we clicked on + let mut c = u16::try_from(clicked.x).unwrap_or(u16::MAX) + 2; + for (i, header) in tabs.iter().enumerate() { + let header_len = width(header, 4) + 1; + c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); + if c == 0 { + // This tab was clicked on + return MouseLocation::Tabs(idx.clone(), i + offset); + } } + // Did not click on a tab + MouseLocation::Out + } else if clicked.y == rows.end.saturating_sub(1) { + // Clicked on status line + MouseLocation::Out + } else if clicked.x < dent { + // Clicked on line numbers + MouseLocation::Out + } else if let Some((fcs, ptr)) = self.files.get_atom(idx.clone()) { + // Clicked on document + let offset = fcs[ptr].doc.offset; + MouseLocation::File( + idx.clone(), + Loc { + x: clicked.x.saturating_sub(dent) + offset.x, + y: clicked.y.saturating_sub(tab) + offset.y, + }, + ) + } else { + // We can't seem to get the atom for some reason, just default to Out + MouseLocation::Out } - // Did not click on a tab - MouseLocation::Out - } else if clicked.y == rows.end.saturating_sub(1) { - // Clicked on status line - MouseLocation::Out - } else if clicked.x < dent { - // Clicked on line numbers - MouseLocation::Out - } else if let Some((fcs, ptr)) = self.files.get_atom(idx.clone()) { - // Clicked on document - let offset = fcs[ptr].doc.offset; - MouseLocation::File( - idx.clone(), - Loc { - x: clicked.x.saturating_sub(dent) + offset.x, - y: clicked.y.saturating_sub(tab) + offset.y, - }, - ) - } else { - // We can't seem to get the atom for some reason, just default to Out - MouseLocation::Out } - } else { - // Pretty sure we just clicked on the file tree (split with no atom!) - MouseLocation::FileTree(row) + Some(FileLayout::FileTree) => MouseLocation::FileTree(row), + Some(FileLayout::Terminal(_)) => MouseLocation::Terminal(idx), + _ => MouseLocation::Out, } } else { MouseLocation::Out @@ -139,6 +144,11 @@ impl Editor { } } } + MouseLocation::Terminal(idx) => { + // Move focus to the index + self.cache_old_ptr(&idx); + self.ptr.clone_from(&idx); + } MouseLocation::Out => (), } } @@ -207,7 +217,8 @@ impl Editor { } MouseLocation::Tabs(_, _) | MouseLocation::Out - | MouseLocation::FileTree(_) => (), + | MouseLocation::FileTree(_) + | MouseLocation::Terminal(_) => (), } } MouseEventKind::Drag(MouseButton::Right) => { @@ -241,7 +252,8 @@ impl Editor { } MouseLocation::Tabs(_, _) | MouseLocation::Out - | MouseLocation::FileTree(_) => (), + | MouseLocation::FileTree(_) + | MouseLocation::Terminal(_) => (), } } // Mouse scroll behaviour diff --git a/src/pty.rs b/src/pty.rs index 913c36df..675ffcc6 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -183,10 +183,7 @@ impl Pty { self.output += &output; Ok(!output.is_empty()) } - Err(e) => { - // Handle polling errors - return Err(e); - } + Err(e) => Err(e), } } } From 86aa29bc630d877e7200901a518022da2cb3065a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 00:42:52 +0000 Subject: [PATCH 17/22] Fixed issue with rust's weird IsADirectory error --- src/editor/mod.rs | 2 +- src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 27aabf82..93eceb1b 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -233,7 +233,7 @@ impl Editor { pub fn open_or_new(&mut self, file_name: String) -> Result<()> { let file = self.open(&file_name); if let Err(OxError::Kaolinite(KError::Io(ref os))) = file { - if os.kind() == ErrorKind::NotFound { + if os.kind() == ErrorKind::NotFound || os.kind() == ErrorKind::IsADirectory { // Create a new document if not found self.blank()?; if let Some((files, _)) = self.files.get_atom_mut(self.ptr.clone()) { diff --git a/src/main.rs b/src/main.rs index 7e8878be..234ce0ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -356,7 +356,7 @@ fn handle_lua_error(key_str: &str, error: RResult<(), mlua::Error>, feedback: &m fn handle_file_opening(editor: &AnyUserData, result: Result<()>, name: &str) { // Block any directories from being opened (we'll wait until file tree is implemented) if file_or_dir(name) == "directory" { - fatal_error(&format!("'{name}' is a directory, not a file")); + fatal_error(&format!("'{name}' is not a file")); } match result { Ok(()) => (), From 405a71209859afc72e30dabca5edfbca1ca0658b Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 00:47:42 +0000 Subject: [PATCH 18/22] Fixed issue where selection in file tree disappears on move --- src/editor/filetree.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/editor/filetree.rs b/src/editor/filetree.rs index 5e8b0323..42e3e120 100644 --- a/src/editor/filetree.rs +++ b/src/editor/filetree.rs @@ -628,8 +628,9 @@ impl Editor { if let Some(old_file) = &self.file_tree_selection.clone() { let path = self.path_prompt()?; if file_or_dir(old_file) == "file" { - std::fs::copy(old_file, path)?; + std::fs::copy(old_file, path.clone())?; self.file_tree_refresh(); + self.file_tree_selection = Some(path.clone()); self.feedback = Feedback::Info("File copied".to_string()); } else { self.feedback = Feedback::Error("Not a file".to_string()); @@ -644,6 +645,7 @@ impl Editor { let path = self.path_prompt()?; std::fs::rename(old_file, path.clone())?; self.file_tree_refresh(); + self.file_tree_selection = Some(path.clone()); if file_or_dir(&path) == "file" { self.feedback = Feedback::Info("File moved".to_string()); } else if file_or_dir(&path) == "directory" { From 0699d81d295fc71693a35a6b164b7725aca25db7 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 01:21:10 +0000 Subject: [PATCH 19/22] Fixed issue with opening file tree on start up --- src/editor/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 93eceb1b..f3a0f523 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -152,11 +152,16 @@ impl Editor { /// Create a blank document if none are already opened pub fn new_if_empty(&mut self) -> Result<()> { - // If no documents were provided, create a new empty document - if self.files.len() == 0 { + let cache = self.ptr.clone(); + // For each atom, ensure they have files in them + while let Some(empty_idx) = self.files.empty_atoms(vec![]) { + // This atom doesn't have a file in it, add a blank one and enable greeting message + self.ptr = empty_idx; self.blank()?; self.greet = config!(self.config, greeting_message).enabled; } + // Restore original pointer position + self.ptr = cache; Ok(()) } From 47033bcddaae2aabc58f2800ff3940253bc2082a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 01:30:02 +0000 Subject: [PATCH 20/22] Force pointer position away from any other weird splits opened in the editor --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index 234ce0ac..fe3162f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,6 +98,12 @@ fn run(cli: &CommandLineInterface) -> Result<()> { &mut ged!(mut &editor).feedback, ); + // Ensure focus is on the initial atom + let init_atom = ged!(&editor).files.empty_atoms(vec![]); + if let Some(init_atom) = init_atom { + ged!(mut &editor).ptr = init_atom; + } + // Load in the file types let file_types = lua .globals() From aa827a9f0db55df7a7cdcf8b19f17cb7c2f2910e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:19:11 +0000 Subject: [PATCH 21/22] Add in file type definition for nushell --- src/plugin/bootstrap.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 7bf00eae..58322154 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -647,6 +647,13 @@ file_types = { modelines = {}, color = "orange", }, + ["Nushell"] = { + icon = " ", + files = {}, + extensions = {"nu"}, + modelines = {}, + color = "green", + }, ["Objective-C"] = { icon = " ", files = {}, From 932ad146be7739b7f46f91a2c091a73bac4c8519 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:32:44 +0000 Subject: [PATCH 22/22] Fixed issue with syntax and undo/redo when overwriting selection --- src/editor/editing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 3391b741..71c90ac1 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -38,8 +38,8 @@ impl Editor { if self.try_doc().is_some() { let doc = self.try_doc().unwrap(); if !doc.is_selection_empty() && !doc.info.read_only { + self.try_doc_mut().unwrap().commit(); self.try_doc_mut().unwrap().remove_selection(); - self.reload_highlight(); } self.new_row()?; // Handle the character insertion