Compare commits
1 Commits
production
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
e74c5ec014 |
20
.replit
Normal file
20
.replit
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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"]
|
@ -1,18 +0,0 @@
|
|||||||
# Introduction
|
|
||||||
Velocity is a fast and hackable text editor made for programming. It is split into two main parts, the backend and the frontend.
|
|
||||||
The backend, written in performant Rust, is the core of the editor and handles text manipulation, files, buffers, views, etc.
|
|
||||||
The frontend, which can be written in any language, is the UI of the editor and handles rendering, keybinds, etc.
|
|
||||||
|
|
||||||
## Editor
|
|
||||||
The `struct Editor` is the main class of the backend. It contains the current state of the editor, the views, and the configuration.
|
|
||||||
The `Editor` is the main way to interact with the backend, and is used by the frontend to manipulate the editor.
|
|
||||||
|
|
||||||
## View
|
|
||||||
The `struct View` defines a way of viewing a buffer, it contains information such as the current cursor locations, the scroll position, and the buffer
|
|
||||||
|
|
||||||
## Buffer
|
|
||||||
The `struct Buffer` contains the text of a file, and is the main way of interacting with the text. It contains a `Vec` of `Line`s, which contain a `Vec` of `char`s.
|
|
||||||
Buffers can be `BufferKind::Read`, meaning the file is read-only, or `BufferKind::Write` allowing edits to be made to the file, or can be `BufferKind::Scratch` which are not associated with a file.
|
|
||||||
|
|
||||||
## Line
|
|
||||||
The `struct Line` contains a `Vec` of `char`s, and is the main way of interacting with the text. It contains a `Vec` of `Line`s, which contain a `Vec` of `char`s.
|
|
9
replit.nix
Normal file
9
replit.nix
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{ pkgs }: {
|
||||||
|
deps = [
|
||||||
|
pkgs.rustc
|
||||||
|
pkgs.rustfmt
|
||||||
|
pkgs.cargo
|
||||||
|
pkgs.cargo-edit
|
||||||
|
pkgs.rust-analyzer
|
||||||
|
];
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
pub mod editor;
|
|
||||||
mod buffer;
|
|
||||||
mod languages;
|
|
||||||
mod config;
|
|
||||||
mod files;
|
|
@ -1,61 +1,37 @@
|
|||||||
use std::error::Error;
|
use std::path::PathBuf;
|
||||||
use crate::core::languages::Language;
|
|
||||||
use crate::core::files;
|
|
||||||
|
|
||||||
enum BufferKind {
|
#[derive(PartialEq)]
|
||||||
Read,
|
pub enum BufferKind {
|
||||||
Write,
|
|
||||||
Scratch,
|
Scratch,
|
||||||
|
Write,
|
||||||
|
Read,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferKind {
|
impl BufferKind {
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
BufferKind::Read => "read",
|
BufferKind::Scratch => "*Scratch*",
|
||||||
BufferKind::Write => "write",
|
BufferKind::Write => "Write",
|
||||||
BufferKind::Scratch => "*scratch*",
|
BufferKind::Read => "Read",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// An file that is open in the editor
|
pub struct Buffer<'a> {
|
||||||
pub struct Buffer {
|
pub data: Vec<String>,
|
||||||
kind: BufferKind,
|
pub path: PathBuf,
|
||||||
lines: Vec<String>,
|
pub kind: BufferKind,
|
||||||
filename: Option<String>,
|
pub name: &'a str,
|
||||||
language: Language,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl<'a> Buffer<'a> {
|
||||||
pub fn new(path: Option<String>) -> Result<Self, Box<dyn Error>> {
|
pub fn new(path: PathBuf, name: &'a str, kind: BufferKind) -> Self {
|
||||||
// If the file exists, read it into the buffer
|
// Return a buffer
|
||||||
if let Some(path) = path {
|
Self {
|
||||||
// Read the file
|
data: vec![String::from("")],
|
||||||
let lines = files::read_file(path.clone())?;
|
path,
|
||||||
|
|
||||||
// Get the language
|
|
||||||
let language = Language::get_language(&path);
|
|
||||||
|
|
||||||
// Check if the file is writeable
|
|
||||||
let kind = if std::path::Path::new(&path).is_file() {
|
|
||||||
BufferKind::Write
|
|
||||||
} else {
|
|
||||||
BufferKind::Read
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
kind,
|
kind,
|
||||||
lines,
|
name,
|
||||||
filename: Some(path),
|
|
||||||
language,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(Self {
|
|
||||||
kind: BufferKind::Scratch,
|
|
||||||
lines: Vec::new(),
|
|
||||||
filename: None,
|
|
||||||
language: Language::PlainText,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,19 +0,0 @@
|
|||||||
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
name: &'static str,
|
|
||||||
version: &'static str,
|
|
||||||
logo: &'static str,
|
|
||||||
motto: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
name: "Velocity",
|
|
||||||
version: VERSION.unwrap_or("UNKNOWN"),
|
|
||||||
logo: "λ",
|
|
||||||
motto: "Hackable text editor for nerds.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +1,55 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
use crate::core::buffer::Buffer;
|
use crate::core::buffer::Buffer;
|
||||||
use crate::core::config::Config;
|
use crate::core::buffer::BufferKind;
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
pub struct Editor {
|
pub struct Config<'a> {
|
||||||
buffers: Vec<Buffer>,
|
pub logo: &'a str,
|
||||||
config: Config,
|
pub friendly_name: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl<'a> Config<'a> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffers: Vec::new(),
|
logo: "λ",
|
||||||
config: Config::new(),
|
friendly_name: "Velocity",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum Mode {
|
||||||
|
Normal,
|
||||||
|
Insert,
|
||||||
|
Select,
|
||||||
|
Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Mode::Normal => "NORMAL",
|
||||||
|
Mode::Insert => "INSERT",
|
||||||
|
Mode::Select => "SELECT",
|
||||||
|
Mode::Command => "COMMAND",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Editor<'a> {
|
||||||
|
pub config: Config<'a>,
|
||||||
|
pub buffer: Box<Buffer<'a>>,
|
||||||
|
pub cursors: Vec<i32>,
|
||||||
|
pub mode: Mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Editor<'a> {
|
||||||
|
pub fn new(path: PathBuf, buffer_name: &'a str) -> Self {
|
||||||
|
let buffer_kind = if path.to_str().unwrap().len() > 1 { BufferKind::Write } else { BufferKind::Scratch };
|
||||||
|
Editor {
|
||||||
|
config: Config::new(),
|
||||||
|
buffer: Box::new(Buffer::new(path, buffer_name, buffer_kind)),
|
||||||
|
cursors: Vec::from([0]),
|
||||||
|
mode: Mode::Normal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attach_file(&mut self) -> Result<(), Box<dyn Error>> {
|
|
||||||
self.buffers.push(Buffer::new(None)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,9 +0,0 @@
|
|||||||
use std::error::Error;
|
|
||||||
|
|
||||||
pub fn read_file(path: String) -> Result<Vec<String>, Box<dyn Error>> {
|
|
||||||
let contents: String = std::fs::read_to_string(path)?;
|
|
||||||
let lines: Vec<String> = contents.lines()
|
|
||||||
.map(|s| s.to_owned())
|
|
||||||
.collect();
|
|
||||||
Ok(lines)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
pub enum Language {
|
|
||||||
Rust,
|
|
||||||
Python,
|
|
||||||
Markdown,
|
|
||||||
PlainText,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Language {
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Language::Rust => "rust",
|
|
||||||
Language::Python => "python",
|
|
||||||
Language::Markdown => "markdown",
|
|
||||||
Language::PlainText => "plain text",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_language(filename: &str) -> Self {
|
|
||||||
let extension = std::path::Path::new(filename)
|
|
||||||
.extension()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
match extension {
|
|
||||||
"rs" => Language::Rust,
|
|
||||||
"py" => Language::Python,
|
|
||||||
"md" => Language::Markdown,
|
|
||||||
_ => Language::PlainText,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
2
src/core/mod.rs
Normal file
2
src/core/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod editor;
|
||||||
|
pub mod buffer;
|
23
src/main.rs
23
src/main.rs
@ -1,6 +1,25 @@
|
|||||||
mod core;
|
mod core;
|
||||||
|
mod terminal;
|
||||||
|
mod tui;
|
||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut editor = core::editor::Editor::new();
|
// Collect command line arguments
|
||||||
editor.attach_file().unwrap();
|
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();
|
||||||
|
|
||||||
|
// Begin velocity
|
||||||
|
tui::ui::start(&mut screen, velocity);
|
||||||
}
|
}
|
1
src/terminal/mod.rs
Normal file
1
src/terminal/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod screen;
|
154
src/terminal/screen.rs
Normal file
154
src/terminal/screen.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||||
|
use crossterm::style::Print;
|
||||||
|
use crossterm::terminal;
|
||||||
|
use crossterm::{execute, ErrorKind};
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
|
// Struct for holding coordinates
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Coords {
|
||||||
|
pub x: usize,
|
||||||
|
pub y: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating a coordinates from two values
|
||||||
|
impl Coords {
|
||||||
|
pub fn from(x: usize, y: usize) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cursor for writing to the terminal screen
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Cursor {
|
||||||
|
pub position: Coords,
|
||||||
|
pub hidden: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a cursor is created
|
||||||
|
impl Cursor {
|
||||||
|
pub fn new() -> Result<Self, ErrorKind> {
|
||||||
|
let mut cursor = Self {
|
||||||
|
position: Coords::from(0, 0),
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
Cursor::move_to(&mut cursor, Coords::from(0, 0));
|
||||||
|
Cursor::hide(&mut cursor);
|
||||||
|
Ok(cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor methods
|
||||||
|
impl Cursor {
|
||||||
|
pub fn move_to(&mut self, position: Coords) {
|
||||||
|
// Set the new position of the cursor
|
||||||
|
self.position = position;
|
||||||
|
|
||||||
|
// Move the cursor to the desired posiition in the terminal
|
||||||
|
execute!(stdout(), MoveTo(position.x as u16, position.y as u16)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(&mut self) {
|
||||||
|
// Remember that the cursor is hidden
|
||||||
|
self.hidden = true;
|
||||||
|
|
||||||
|
// Hide the cursor from the terminal screen
|
||||||
|
execute!(stdout(), Hide).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self) {
|
||||||
|
// Remember that the cursor isn't hidden
|
||||||
|
self.hidden = false;
|
||||||
|
|
||||||
|
// Show the cursor to the terminal screen
|
||||||
|
execute!(stdout(), Show).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A struct for holding the size of the terminal
|
||||||
|
pub struct Size {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The terminal screen
|
||||||
|
pub struct Screen {
|
||||||
|
pub size: Size,
|
||||||
|
pub cursor: Cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For when a new terminal screen is created
|
||||||
|
impl Screen {
|
||||||
|
pub fn new() -> Result<Self, ErrorKind> {
|
||||||
|
// Get the size of the terminal
|
||||||
|
let size = terminal::size()?;
|
||||||
|
|
||||||
|
// Define a new terminal screen struct
|
||||||
|
let mut screen = Self {
|
||||||
|
size: Size {
|
||||||
|
width: size.0 as usize,
|
||||||
|
height: size.1 as usize,
|
||||||
|
},
|
||||||
|
cursor: Cursor::new().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty the terminal screen
|
||||||
|
Screen::clear();
|
||||||
|
|
||||||
|
// Enter the terminal screen
|
||||||
|
screen.enter();
|
||||||
|
|
||||||
|
// Return a result containing the terminal
|
||||||
|
Ok(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal functions and methods for managing the terminal
|
||||||
|
impl Screen {
|
||||||
|
pub fn refresh(&mut self) -> Result<(), ErrorKind>{
|
||||||
|
// Clear the screen
|
||||||
|
Screen::clear();
|
||||||
|
|
||||||
|
// Update the screen dimensions
|
||||||
|
let size = terminal::size()?;
|
||||||
|
self.size.width = size.0 as usize;
|
||||||
|
self.size.height = size.1 as usize;
|
||||||
|
|
||||||
|
// Return Ok if was successful
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enter(&mut self) {
|
||||||
|
// Hide the cursor
|
||||||
|
self.cursor.hide();
|
||||||
|
|
||||||
|
// Enter the terminal screen
|
||||||
|
terminal::enable_raw_mode().unwrap();
|
||||||
|
execute!(stdout(), terminal::EnterAlternateScreen).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit(&mut self) {
|
||||||
|
// Show the cursor
|
||||||
|
self.cursor.show();
|
||||||
|
|
||||||
|
// Exit the terminal screen
|
||||||
|
execute!(stdout(), terminal::LeaveAlternateScreen).unwrap();
|
||||||
|
terminal::disable_raw_mode().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
// Clears the terminal screen
|
||||||
|
execute!(stdout(), terminal::Clear(terminal::ClearType::All)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(text: &str) {
|
||||||
|
// Writes a line to a current cursor position
|
||||||
|
execute!(stdout(), Print(text)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_at(&mut self, text: String, position: Coords) {
|
||||||
|
// Writes a line at a set of coordinates
|
||||||
|
self.cursor.move_to(position);
|
||||||
|
Screen::write(&text);
|
||||||
|
}
|
||||||
|
}
|
2
src/tui/components/mod.rs
Normal file
2
src/tui/components/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod statusbar;
|
||||||
|
pub mod welcome;
|
63
src/tui/components/statusbar.rs
Normal file
63
src/tui/components/statusbar.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use crossterm::style::Stylize;
|
||||||
|
use crate::core::editor::Editor;
|
||||||
|
use crate::terminal::screen::{Coords, Screen};
|
||||||
|
use crate::tui::utils;
|
||||||
|
|
||||||
|
pub fn draw(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 = &utils::with_spaces(editor.config.logo) as &str;
|
||||||
|
// Get the current mode into a string
|
||||||
|
let mode_string = &utils::with_spaces(editor.mode.as_str()) as &str;
|
||||||
|
// Get the current open file name
|
||||||
|
let file_name = &utils::with_spaces(editor.buffer.name) as &str;
|
||||||
|
// 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
|
||||||
|
let total_length = editor_logo.len() + mode_string.len() + file_name.len() + buffer_kind.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),
|
||||||
|
);
|
||||||
|
|
||||||
|
let x = x + mode_string.len();
|
||||||
|
// Draws the file name if it has a length, if not then it will draw the buffer type
|
||||||
|
if editor.buffer.name.len() > 0 {
|
||||||
|
screen.write_at(
|
||||||
|
file_name.magenta().bold().reverse().to_string(),
|
||||||
|
Coords::from(x, status_height),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
screen.write_at(
|
||||||
|
buffer_kind.blue().bold().reverse().to_string(),
|
||||||
|
Coords::from(x, status_height),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
39
src/tui/components/welcome.rs
Normal file
39
src/tui/components/welcome.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use crossterm::style::Stylize;
|
||||||
|
use crate::core::editor::Editor;
|
||||||
|
use crate::terminal::screen::{Coords, Screen};
|
||||||
|
use crate::tui::utils;
|
||||||
|
|
||||||
|
pub fn draw(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 the editor",
|
||||||
|
];
|
||||||
|
|
||||||
|
// If the screen is big enough, we can draw
|
||||||
|
if screen.size.width > utils::longest_element_in_vec(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 = utils::calc_centred_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 = utils::calc_centred_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/tui/mod.rs
Normal file
3
src/tui/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod ui;
|
||||||
|
pub mod utils;
|
||||||
|
mod components;
|
38
src/tui/ui.rs
Normal file
38
src/tui/ui.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
use crate::core::editor::Editor;
|
||||||
|
use crate::core::buffer::BufferKind;
|
||||||
|
use crate::terminal::screen::Screen;
|
||||||
|
use crate::tui::components;
|
||||||
|
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
pub fn start(screen: &mut Screen, editor: Editor) {
|
||||||
|
// Main screen loop, runs until the program exits
|
||||||
|
loop {
|
||||||
|
// Refresh the screen
|
||||||
|
screen.refresh().unwrap();
|
||||||
|
|
||||||
|
// Draw the welcome message only if it is a scratch buffer
|
||||||
|
if editor.buffer.kind == BufferKind::Scratch {
|
||||||
|
components::welcome::draw(screen, &editor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw the status bar
|
||||||
|
components::statusbar::draw(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();
|
||||||
|
}
|
14
src/tui/utils.rs
Normal file
14
src/tui/utils.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Surrounds a &str with spaces
|
||||||
|
pub fn with_spaces(text: &str) -> String {
|
||||||
|
format!(" {} ", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the starting x coordinate for centred text
|
||||||
|
pub fn calc_centred_x(screen_width: usize, item_length: usize) -> usize {
|
||||||
|
(screen_width / 2) - (item_length / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the longest element in a vector
|
||||||
|
pub fn longest_element_in_vec(elements: Vec<&str>) -> usize {
|
||||||
|
elements.iter().max_by_key(|x: &&&str| x.len()).unwrap().len()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user