Compare commits

..

No commits in common. "7ff58d6f760b181b81cac040980d7d27a3eb948a" and "07e489e825186cc45061f395059acbe7f7e7cde8" have entirely different histories.

19 changed files with 290 additions and 201 deletions

20
.replit
View File

@ -1,20 +0,0 @@
run = "cargo run"
hidden = ["target"]
[packager]
language = "rust"
[packager.features]
packageSearch = true
[languages.rust]
pattern = "**/*.rs"
[languages.rust.languageServer]
start = "rust-analyzer"
[nix]
channel = "stable-22_05"
[gitHubImport]
requiredFiles = [".replit", "replit.nix"]

14
Cargo.lock generated
View File

@ -45,6 +45,13 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "lambda"
version = "0.1.0"
dependencies = [
"crossterm",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.137" version = "0.2.137"
@ -156,13 +163,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "velocity"
version = "0.1.0"
dependencies = [
"crossterm",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -1,11 +1,11 @@
[package] [package]
name = "velocity" name = "lambda"
version = "0.1.0" version = "0.1.0"
authors = ["Madeline <maddie@spyhoodle.me>"] authors = ["Madeline <maddie@spyhoodle.me>"]
edition = "2021" edition = "2021"
description = "Next generation hackable text editor for nerds" description = "Next generation hackable text editor for nerds"
homepage = "https://github.com/SpyHoodle/velocity" homepage = "https://github.com/SpyHoodle/lambda"
repository = "https://github.com/SpyHoodle/velocity" repository = "https://github.com/SpyHoodle/lambda"
readme = "README.md" readme = "README.md"
include = ["src/*.rs", "Cargo.toml"] include = ["src/*.rs", "Cargo.toml"]
categories = ["text-editors"] categories = ["text-editors"]

View File

@ -1,29 +1,29 @@
# λ Velocity # λ Lambda
A next-generation hackable incredibly performant rust text editor for nerds. A next-generation hackable incredibly performant rust text editor for nerds.
> ⚠️ Velocity is in *very* early stages at the moment. Velocity's goals are still being decided and features may completely change. > ⚠️ Lambda is in *very* early stages at the moment. Lambda's goals are still being decided and features may completely change.
## Overview ## Overview
Velocity is a similar text editor to `vim` or `kakoune`, taking some ideas from `xi`. Lambda is a similar text editor to `vim` or `kakoune`, taking some ideas from `xi`.
The main goal is to build the best text editor possible by taking ideas from existing text editors and implementing them in the best possible way. The main goal is to build the best text editor possible by taking ideas from existing text editors and implementing them in the best possible way.
- Velocity is written in Rust, so it's incredibly fast and logical - Lambda is written in Rust, so it's incredibly fast and logical
- It's also full of comments, so anyone can try and learn what it's doing - It's also full of comments, so anyone can try and learn what it's doing
- Velocity is very modular and extensible, so features can be easily added through a variety of methods - Lambda is very modular and extensible, so features can be easily added through a variety of methods
- Need to run a set of keybinds in order? Create a macro - Need to run a set of keybinds in order? Create a macro
- Need to create a completely new feature? Just fork it and write it in Rust - Need to create a completely new feature? Just fork it and write it in Rust
- Velocity is separated between the core and the standard terminal implementation - Lambda is separated between the core and the standard terminal implementation
- This means that anyone can implement their own keybinds, ui, themes, styling, etc. - This means that anyone can implement their own keybinds, ui, themes, styling, etc.
- This also means that there is one standard way for managing the text itself - inside of the Velocity core - This also means that there is one standard way for managing the text itself - inside of the lambda core
- Velocity is a modal text editor and uses ideas from kakoune, and is designed for the select -> action structure - Lambda is a modal text editor and uses ideas from kakoune, and is designed for the select -> action structure
- Since anyone can implement their own keybinds, it's possible to make a vim implementation that uses the action -> select structure - Since anyone can implement their own keybinds, it's possible to make a vim implementation that uses the action -> select structure
- Velocity follows the unix philosophy of "do one thing and do it well" - Lambda follows the unix philosophy of "do one thing and do it well"
- It has no bloated features like splits or tabs - It has no bloated features like splits or tabs
- It contains the bare necessities and provides a few extra modules - It contains the bare necessities and provides a few extra modules
- Velocity has much better default keybindings than other text editors - Lambda has much better default keybindings than other text editors
## Getting started ## Getting started
You'll need `cargo` (ideally from `rustup`) and an up to date version of `rust`. You'll need `cargo` (ideally from `rustup`) and an up to date version of `rust`.
```bash ```bash
git clone https://github.com/SpyHoodle/velocity.git # Clone the repositiory git clone https://github.com/SpyHoodle/lambda.git # Clone the repositiory
cargo run # Build and run velocity! cargo run # Build and run lambda!
``` ```

View File

@ -0,0 +1,2 @@
- [ ] Modularise functions
- [ ] Make components (i.e statusbar) modular

View File

@ -1,9 +0,0 @@
{ pkgs }: {
deps = [
pkgs.rustc
pkgs.rustfmt
pkgs.cargo
pkgs.cargo-edit
pkgs.rust-analyzer
];
}

View File

@ -1,37 +0,0 @@
use std::path::PathBuf;
#[derive(PartialEq)]
pub enum BufferKind {
Scratch,
Write,
Read,
}
impl BufferKind {
pub fn as_str(&self) -> &str {
match self {
BufferKind::Scratch => "*scratch*",
BufferKind::Write => "write",
BufferKind::Read => "read",
}
}
}
pub struct Buffer<'a> {
pub data: Vec<String>,
pub path: PathBuf,
pub kind: BufferKind,
pub name: &'a str,
}
impl<'a> Buffer<'a> {
pub fn new(path: PathBuf, name: &'a str, kind: BufferKind) -> Self {
// Return a buffer
Self {
data: vec![String::from("")],
path,
kind,
name,
}
}
}

View File

@ -1,7 +1,3 @@
use std::path::PathBuf;
use crate::core::buffer::Buffer;
use crate::core::buffer::BufferKind;
pub struct Config<'a> { pub struct Config<'a> {
pub logo: &'a str, pub logo: &'a str,
pub friendly_name: &'a str, pub friendly_name: &'a str,
@ -11,11 +7,17 @@ impl<'a> Config<'a> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
logo: "λ", logo: "λ",
friendly_name: "Velocity", friendly_name: "Lambda",
} }
} }
} }
pub struct Buffer<'a> {
pub data: Vec<String>,
pub name: &'a str,
pub path: &'a str,
}
#[allow(dead_code)] #[allow(dead_code)]
pub enum Mode { pub enum Mode {
Normal, Normal,
@ -37,17 +39,20 @@ impl Mode {
pub struct Editor<'a> { pub struct Editor<'a> {
pub config: Config<'a>, pub config: Config<'a>,
pub buffer: Box<Buffer<'a>>, pub buffer: Buffer<'a>,
pub cursors: Vec<i32>, pub cursors: Vec<i32>,
pub mode: Mode, pub mode: Mode,
} }
impl<'a> Editor<'a> { impl<'a> Editor<'a> {
pub fn new(path: PathBuf, buffer_name: &'a str) -> Self { pub fn new() -> Self {
let buffer_kind = if path.to_str().unwrap().len() > 1 { BufferKind::Write } else { BufferKind::Scratch };
Editor { Editor {
config: Config::new(), config: Config::new(),
buffer: Box::new(Buffer::new(path, buffer_name, buffer_kind)), buffer: Buffer {
data: Vec::from([String::from("Hello"), String::from("World")]),
name: "[No Name]",
path: "/home/spy",
},
cursors: Vec::from([0]), cursors: Vec::from([0]),
mode: Mode::Normal, mode: Mode::Normal,
} }

View File

@ -1,2 +1 @@
pub mod editor; pub mod editor;
pub mod buffer;

View File

@ -1,25 +1,9 @@
mod core; mod core;
mod terminal; mod terminal;
mod tui; mod tui;
use std::{env, path::PathBuf};
fn main() { fn main() {
// Collect command line arguments let lambda = core::editor::Editor::new();
let args: Vec<String> = env::args().collect();
// Collect the file path
let file_path = if args.len() > 1 { PathBuf::from(&args[1]) } else { PathBuf::from("") };
// Collect the file name
let file_name = file_path.clone();
let file_name = if args.len() > 1 { file_name.file_name().unwrap().to_str().unwrap() } else { "" };
// Initalise a new editor
let velocity = core::editor::Editor::new(file_path, file_name);
// Initalise a screen
let mut screen = terminal::screen::Screen::new().unwrap(); let mut screen = terminal::screen::Screen::new().unwrap();
tui::ui::Ui::run(&mut screen, lambda);
// Begin lambda
tui::ui::start(&mut screen, velocity);
} }

View File

@ -1 +1 @@
pub mod screen; pub mod screen;

View File

@ -11,7 +11,7 @@ pub struct Coords {
pub y: usize, pub y: usize,
} }
// Creating a coordinates from two values // Creating a set of coordinates from two values
impl Coords { impl Coords {
pub fn from(x: usize, y: usize) -> Self { pub fn from(x: usize, y: usize) -> Self {
Self { x, y } Self { x, y }

131
src/terminal/tui.rs Normal file
View File

@ -0,0 +1,131 @@
use crate::core::editor::Editor;
use crate::terminal::screen::{Coords, Screen};
use crossterm::style::Stylize;
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
// Utils
fn with_spaces(text: &str) -> String {
format!(" {} ", text)
}
fn calc_x(screen_width: usize, item_length: usize) -> usize {
(screen_width / 2) - (item_length / 2)
}
fn longest_element_in_array(elements: Vec<&str>) -> usize {
elements.iter().max_by_key(|x: &&&str| x.len()).unwrap().len()
}
pub fn draw_status(screen: &mut Screen, editor: &Editor) -> Result<(), ()> {
// Calculate where to draw the status bar
let status_height = screen.size.height - 2;
// Get the editor logo from the config
let editor_logo = &with_spaces(editor.config.logo) as &str;
// Get the current mode into a string
let mode_string = &with_spaces(editor.mode.as_str()) as &str;
// Get the current open file name
let file_name = &with_spaces(editor.buffer.name) as &str;
// Calculate the total length of all the status bar components
let total_length = editor_logo.len() + mode_string.len() + file_name.len() + 1;
// If the screen isn't wide enough, panic as we can't draw the status bar
if screen.size.width < total_length {
Err(())
} else {
// Write the editor logo
screen.write_at(
editor_logo.yellow().bold().reverse().to_string(),
Coords::from(0, status_height),
);
// Calculate where to write the current mode
let x = editor_logo.len() - 1;
// Write the current mode
screen.write_at(
mode_string.green().bold().reverse().to_string(),
Coords::from(x, status_height),
);
// Calculate where to write the file name
let x = x + mode_string.len();
// Write the current file name
screen.write_at(
file_name.magenta().bold().reverse().to_string(),
Coords::from(x, status_height),
);
// Draw the rest of the status bar
let x = x + file_name.len();
screen.write_at(
" ".repeat(screen.size.width - x).reverse().to_string(),
Coords::from(x, status_height),
);
Ok(())
}
}
pub fn draw_welcome(screen: &mut Screen, editor: &Editor) {
// The welcome message
let title = format!("{} {}", editor.config.logo, editor.config.friendly_name);
let message: [&str; 5] = [
"Hackable text editor for nerds",
"",
"Type :help to open the README.md document",
"Type :o <file> to open a file and edit",
"Type :q! or <C-c> to quit lambda",
];
// If the screen is big enough, we can draw
if screen.size.width > longest_element_in_array(message.to_vec()) && screen.size.height > message.len() + 4 {
// The starting y position in the centre of the screen
let mut y = (screen.size.height / 2) - (message.len() / 2) - 2;
// Calculate where to place the title
let x = calc_x(screen.size.width, title.len());
// Write the title to the screen
screen.write_at(title.yellow().to_string(), Coords::from(x, y));
for line in message {
// Each line has different width so requires a different x position to center it
let x = calc_x(screen.size.width, line.len()) ;
// For each line we move downwards so increment y
y += 1;
// Write the line to the screen at position (x, y)
screen.write_at(line.to_string(), Coords::from(x, y));
}
}
}
pub fn start(screen: &mut Screen, editor: Editor) {
loop {
// Refresh the screen
screen.refresh().unwrap();
// Draw the welcome message
draw_welcome(screen, &editor);
// Draw the status bar
draw_status(screen, &editor).unwrap();
// Check for any key presses
match read().unwrap() {
Event::Key(KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
..
}) => break,
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => break,
_ => (),
}
}
screen.exit();
}

View File

@ -1,2 +1,2 @@
pub mod statusbar; pub mod statusbar;
pub mod welcome; pub mod welcome;

View File

@ -1,63 +1,74 @@
use crossterm::style::Stylize; use crate::tui::ui::Component;
use crate::tui::utils::with_spaces;
use crate::core::editor::Editor; use crate::core::editor::Editor;
use crate::terminal::screen::{Coords, Screen}; use crate::terminal::screen::{Coords, Screen};
use crate::tui::utils; use crossterm::style::Stylize;
pub fn draw(screen: &mut Screen, editor: &Editor) -> Result<(), ()> { pub struct StatusBar<'a> {
// Calculate where to draw the status bar logo: &'a str,
let status_height = screen.size.height - 2; mode: &'a str,
file_name: &'a str,
}
// Get the editor logo from the config impl<'a> StatusBar<'a> {
let editor_logo = &utils::with_spaces(editor.config.logo) as &str; pub fn new(editor: &'a Editor<'a>) -> Self {
// Get the current mode into a string Self {
let mode_string = &utils::with_spaces(editor.mode.as_str()) as &str; logo: editor.config.logo,
// Get the current open file name mode: editor.mode.as_str(),
let file_name = &utils::with_spaces(editor.buffer.name) as &str; file_name: editor.buffer.name,
// Get the current buffer kind }
let buffer_kind = &utils::with_spaces(editor.buffer.kind.as_str()) as &str; }
}
// Calculate the total length of all the status bar components impl<'a> Component for StatusBar<'a> {
let total_length = editor_logo.len() + mode_string.len() + file_name.len() + buffer_kind.len() + 1; fn draw(&self, screen: &mut Screen, editor: &Editor) -> Result<(), ()> {
// Calculate where to draw the status bar
let status_height = screen.size.height - 2;
// If the screen isn't wide enough, panic as we can't draw the status bar // Get the editor logo from the config
if screen.size.width < total_length { let editor_logo = &with_spaces(editor.config.logo) as &str;
Err(()) // Get the current mode into a string
let mode_string = &with_spaces(editor.mode.as_str()) as &str;
// Get the current open file name
let file_name = &with_spaces(editor.buffer.name) as &str;
} else { // Calculate the total length of all the status bar components
// Write the editor logo let total_length = editor_logo.len() + mode_string.len() + file_name.len() + 1;
screen.write_at(
editor_logo.yellow().bold().reverse().to_string(),
Coords::from(0, status_height),
);
// Calculate where to write the current mode // If the screen isn't wide enough, panic as we can't draw the status bar
let x = editor_logo.len() - 1; if screen.size.width < total_length {
// Write the current mode Err(())
screen.write_at(
mode_string.green().bold().reverse().to_string(),
Coords::from(x, status_height),
);
let x = x + mode_string.len(); } else {
// Draws the file name if it has a length, if not then it will draw the buffer type // Write the editor logo
if editor.buffer.name.len() > 0 { screen.write_at(
editor_logo.yellow().bold().reverse().to_string(),
Coords::from(0, status_height),
);
// Calculate where to write the current mode
let x = editor_logo.len() - 1;
// Write the current mode
screen.write_at(
mode_string.green().bold().reverse().to_string(),
Coords::from(x, status_height),
);
// Calculate where to write the file name
let x = x + mode_string.len();
// Write the current file name
screen.write_at( screen.write_at(
file_name.magenta().bold().reverse().to_string(), file_name.magenta().bold().reverse().to_string(),
Coords::from(x, status_height), Coords::from(x, status_height),
); );
} else {
// Draw the rest of the status bar
let x = x + file_name.len();
screen.write_at( screen.write_at(
buffer_kind.blue().bold().reverse().to_string(), " ".repeat(screen.size.width - x).reverse().to_string(),
Coords::from(x, status_height), Coords::from(x, status_height),
); );
Ok(())
} }
let x = if editor.buffer.name.len() > 0 { x + file_name.len() } else { x + buffer_kind.len() };
screen.write_at(
" ".repeat(screen.size.width - x).reverse().to_string(),
Coords::from(x, status_height),
);
Ok(())
} }
} }

View File

@ -1,9 +1,9 @@
use crossterm::style::Stylize; use crate::tui::utils;
use crate::core::editor::Editor; use crate::core::editor::Editor;
use crate::terminal::screen::{Coords, Screen}; use crate::terminal::screen::{Coords, Screen};
use crate::tui::utils; use crossterm::style::Stylize;
pub fn draw(screen: &mut Screen, editor: &Editor) { pub fn draw_welcome(screen: &mut Screen, editor: &Editor) {
// The welcome message // The welcome message
let title = format!("{} {}", editor.config.logo, editor.config.friendly_name); let title = format!("{} {}", editor.config.logo, editor.config.friendly_name);
let message: [&str; 5] = [ let message: [&str; 5] = [
@ -11,7 +11,7 @@ pub fn draw(screen: &mut Screen, editor: &Editor) {
"", "",
"Type :help to open the README.md document", "Type :help to open the README.md document",
"Type :o <file> to open a file and edit", "Type :o <file> to open a file and edit",
"Type :q! or <C-c> to quit the editor", "Type :q! or <C-c> to quit lambda",
]; ];
// If the screen is big enough, we can draw // If the screen is big enough, we can draw
@ -20,14 +20,14 @@ pub fn draw(screen: &mut Screen, editor: &Editor) {
let mut y = (screen.size.height / 2) - (message.len() / 2) - 2; let mut y = (screen.size.height / 2) - (message.len() / 2) - 2;
// Calculate where to place the title // Calculate where to place the title
let x = utils::calc_centred_x(screen.size.width, title.len()); let x = utils::calc_centered_x(screen.size.width, title.len());
// Write the title to the screen // Write the title to the screen
screen.write_at(title.yellow().to_string(), Coords::from(x, y)); screen.write_at(title.yellow().to_string(), Coords::from(x, y));
for line in message { for line in message {
// Each line has different width so requires a different x position to center it // Each line has different width so requires a different x position to center it
let x = utils::calc_centred_x(screen.size.width, line.len()) ; let x = utils::calc_centered_x(screen.size.width, line.len()) ;
// For each line we move downwards so increment y // For each line we move downwards so increment y
y += 1; y += 1;
@ -37,3 +37,4 @@ pub fn draw(screen: &mut Screen, editor: &Editor) {
} }
} }
} }

View File

@ -1,3 +1,3 @@
pub mod ui; pub mod ui;
pub mod utils; pub mod components;
mod components; mod utils;

View File

@ -1,38 +1,60 @@
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
use crate::core::editor::Editor; use crate::core::editor::Editor;
use crate::core::buffer::BufferKind;
use crate::terminal::screen::Screen; use crate::terminal::screen::Screen;
use crate::tui::components; use crate::tui::components;
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
pub fn start(screen: &mut Screen, editor: Editor) { pub trait Component {
// Main screen loop, runs until the program exits fn draw(&self, screen: &mut Screen, editor: &Editor) -> Result<(), ()>;
loop { }
// Refresh the screen
screen.refresh().unwrap();
// Draw the welcome message only if it is a scratch buffer struct Components {
if editor.buffer.kind == BufferKind::Scratch { bottom: Vec<Box<dyn Component>>,
components::welcome::draw(screen, &editor); }
};
// Draw the status bar pub struct Ui {
components::statusbar::draw(screen, &editor).unwrap(); components: Components,
}
// Check for any key presses impl Ui {
match read().unwrap() { pub fn new<'a>(editor: &'a Editor<'a>) -> Self {
Event::Key(KeyEvent { let status_bar = components::statusbar::StatusBar::new(editor);
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL, Self {
.. components: Components { bottom: vec![Box::new(status_bar)]},
}) => break,
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => break,
_ => (),
} }
} }
screen.exit(); pub fn draw(&self) {
}
pub fn run(screen: &mut Screen, editor: Editor) {
loop {
// Refresh the screen
screen.refresh().unwrap();
// Generate all the UI elements
let components = Ui::new(&editor);
// Draw all UI elements
components.draw();
// Check for any key presses
match read().unwrap() {
Event::Key(KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
..
}) => break,
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => break,
_ => (),
}
}
screen.exit();
}
} }

View File

@ -3,12 +3,12 @@ pub fn with_spaces(text: &str) -> String {
format!(" {} ", text) format!(" {} ", text)
} }
// Calculates the starting x coordinate for centred text // Calculates the x coordinate for centered text
pub fn calc_centred_x(screen_width: usize, item_length: usize) -> usize { pub fn calc_centered_x(screen_width: usize, item_length: usize) -> usize {
(screen_width / 2) - (item_length / 2) (screen_width / 2) - (item_length / 2)
} }
// Returns the longest element in a vector // Finds and returns the longest element in a vector
pub fn longest_element_in_vec(elements: Vec<&str>) -> usize { pub fn longest_element_in_vec(elements: Vec<&str>) -> usize {
elements.iter().max_by_key(|x: &&&str| x.len()).unwrap().len() elements.iter().max_by_key(|x: &&&str| x.len()).unwrap().len()
} }