commit
8857c6a99f
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,2 @@
|
||||
__pycache__
|
||||
core/__pycache__
|
||||
mode/__pycache__
|
||||
.idea
|
||||
target
|
||||
|
249
Cargo.lock
generated
Normal file
249
Cargo.lock
generated
Normal file
@ -0,0 +1,249 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lambda"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "lambda"
|
||||
version = "0.1.0"
|
||||
authors = ["Madeline <maddie@spyhoodle.me>"]
|
||||
edition = "2021"
|
||||
description = "Next generation hackable text editor for nerds"
|
||||
homepage = "https://github.com/SpyHoodle/lambda"
|
||||
repository = "https://github.com/SpyHoodle/lambda"
|
||||
readme = "README.md"
|
||||
include = ["src/*.rs", "Cargo.toml"]
|
||||
categories = ["text-editors"]
|
||||
keywords = ["text-editor", "editor", "terminal", "tui"]
|
||||
|
||||
[profile.release]
|
||||
panic = abort
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.25"
|
54
README.md
54
README.md
@ -1,35 +1,29 @@
|
||||
# λ lambda
|
||||
# λ Lambda
|
||||
A next-generation hackable incredibly performant rust text editor for nerds.
|
||||
> ⚠️ Lambda is in *very* early stages at the moment. Lambda's goals are still being decided and features may completely change.
|
||||
|
||||
Next generation hackable text editor for nerds.
|
||||
## Overview
|
||||
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.
|
||||
|
||||
### Let it be known!
|
||||
|
||||
Lambda is in *very* early stages at the moment. Features may change completely, or even be removed.<br>
|
||||
Don't expect lambda to stay the way it is. Updates are pushed often.
|
||||
|
||||
### Overview
|
||||
|
||||
Lambda is a similar text editor to `vim` or `kakoune`.<br>
|
||||
However, it takes a different approach to most of the features seen in other editors.
|
||||
|
||||
- Lambda is written in Python, so it is easy to hack and learn.
|
||||
- It also has a good amount of comments!
|
||||
- Lambda is incredibly modular, so you can easily add new features.
|
||||
- Lambda follows the unix philosophy of "do one thing and do it well."
|
||||
- It has no bloated features, like splits or tabs
|
||||
- It contains the bare necessities and provides a few extra modules
|
||||
- Lambda isn't limited to modes or keybindings.
|
||||
- Keybindings and modes can be easily changed
|
||||
- Other modes can be used by holding down keybindings (i.e. `ctrl-x` inside of `insert` mode)
|
||||
- Lambda is extremely fast and makes use of efficient memory management.
|
||||
- Neovim is slow, and actually requires [a plugin to speed it up](https://github.com/lewis6991/impatient.nvim).
|
||||
- Lambda has much better default keybindings than other text editors.
|
||||
|
||||
### Getting started
|
||||
- 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
|
||||
- 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 create a completely new feature? Just fork it and write it in Rust
|
||||
- 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 also means that there is one standard way for managing the text itself - inside of the lambda core
|
||||
- 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
|
||||
- Lambda follows the unix philosophy of "do one thing and do it well"
|
||||
- It has no bloated features like splits or tabs
|
||||
- It contains the bare necessities and provides a few extra modules
|
||||
- Lambda has much better default keybindings than other text editors
|
||||
|
||||
## Getting started
|
||||
You'll need `cargo` (ideally from `rustup`) and an up to date version of `rust`.
|
||||
```bash
|
||||
git clone https://github.com/SpyHoodle/lambda.git # Clone the repository
|
||||
cd lambda # Enter lambda directory
|
||||
chmod +x install.sh # Make the install script executable
|
||||
./install.sh # Run the install script
|
||||
git clone https://github.com/SpyHoodle/lambda.git # Clone the repositiory
|
||||
cargo run # Build and run lambda!
|
||||
```
|
3
TODO.md
3
TODO.md
@ -1 +1,2 @@
|
||||
## TODO
|
||||
- [ ] Make components (i.e statusbar) modular
|
||||
- [ ] Modularise functions
|
||||
|
@ -1,90 +0,0 @@
|
||||
import os
|
||||
|
||||
|
||||
class Buffer:
|
||||
def __init__(self, path: str, name: str = None, data: list = None):
|
||||
self.path = path
|
||||
self.name = name or "[No Name]"
|
||||
self.data = data or [""]
|
||||
|
||||
def render(self, instance):
|
||||
for y, line in enumerate(self.data[instance.offset[0]:]):
|
||||
if y <= instance.safe_height:
|
||||
for x, character in enumerate(line[instance.offset[1]:]):
|
||||
if x <= instance.safe_width:
|
||||
instance.screen.addstr(y, x + instance.components.get_component_width(
|
||||
instance.components.components["left"]), character)
|
||||
|
||||
# Write blank spaces for the rest of the line
|
||||
if instance.safe_width - len(line) > 0:
|
||||
instance.screen.addstr(y, instance.components.get_component_width(
|
||||
instance.components.components["left"]) + len(line), " " * (instance.safe_width - len(line)))
|
||||
|
||||
@staticmethod
|
||||
def delete_line(instance, y: int = None):
|
||||
# Default to the cursor position
|
||||
y = y or instance.cursor[0]
|
||||
|
||||
# Remove a line from the buffer
|
||||
instance.buffer.data.pop(y)
|
||||
|
||||
@staticmethod
|
||||
def insert_line(instance, y: int = None):
|
||||
# Default to the cursor position
|
||||
y = y or instance.cursor[0]
|
||||
|
||||
# Insert a line into the buffer
|
||||
instance.buffer.data.insert(y, "")
|
||||
|
||||
@staticmethod
|
||||
def delete_char(instance, y: int = None, x: int = None):
|
||||
# Default to the cursor position
|
||||
y = y or instance.cursor[0]
|
||||
x = x or instance.cursor[1]
|
||||
|
||||
# Remove a character from the line at a given index
|
||||
instance.buffer.data[y] = instance.buffer.data[y][:x - 1] + instance.buffer.data[y][x:]
|
||||
|
||||
@staticmethod
|
||||
def insert_char(instance, char: (str, chr), y: int = None, x: int = None):
|
||||
# Default to the cursor position
|
||||
y = y or instance.cursor[0]
|
||||
x = x or instance.cursor[1]
|
||||
|
||||
# Insert a character into the line at a given index
|
||||
instance.buffer.data[y] = instance.buffer.data[y][:x] + char + instance.buffer.data[y][x:]
|
||||
|
||||
|
||||
def open_file(file_path):
|
||||
# Open the file
|
||||
with open(file_path) as f:
|
||||
# Convert it into a list of lines
|
||||
lines = f.readlines()
|
||||
|
||||
# Add a line if the file is empty or if the last line is not empty
|
||||
if lines[-1].endswith("\n") or not len(lines):
|
||||
lines.append("")
|
||||
|
||||
# Remove the newlines
|
||||
lines = [line.rstrip("\n") for line in lines]
|
||||
|
||||
# Return the list of lines
|
||||
return lines
|
||||
|
||||
|
||||
def load_file(file_path=None):
|
||||
# Default settings for a file
|
||||
file_name = "[No Name]"
|
||||
file_data = [""]
|
||||
|
||||
if file_path:
|
||||
# Set the file's name
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
# Only if the file actually exists
|
||||
if os.path.exists(file_path):
|
||||
# Open the file as a list of lines
|
||||
file_data = open_file(file_path)
|
||||
|
||||
# Return a dictionary which will become all the data about the buffer
|
||||
return Buffer(file_path, file_name, file_data)
|
@ -1,51 +0,0 @@
|
||||
import curses
|
||||
|
||||
|
||||
class Codes:
|
||||
# Color codes
|
||||
red = '\033[91m'
|
||||
green = '\033[92m'
|
||||
blue = '\033[94m'
|
||||
yellow = '\033[93m'
|
||||
cyan = '\033[96m'
|
||||
magenta = '\033[95m'
|
||||
white = '\033[97m'
|
||||
selected_white = '\033[47m'
|
||||
selected_green = '\033[42m'
|
||||
strike = '\033[9m'
|
||||
italic = '\033[3m'
|
||||
end = '\033[0m'
|
||||
|
||||
|
||||
def init_colors():
|
||||
# Activate color support
|
||||
curses.start_color()
|
||||
|
||||
# Foreground: WHITE, Background: BLACK
|
||||
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: WHITE
|
||||
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
||||
# Foreground: RED, Background: BLACK
|
||||
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: RED
|
||||
curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_RED)
|
||||
# Foreground: GREEN, Background: BLACK
|
||||
curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: GREEN
|
||||
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_GREEN)
|
||||
# Foreground: YELLOW, Background: BLACK
|
||||
curses.init_pair(7, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: YELLOW
|
||||
curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_YELLOW)
|
||||
# Foreground: CYAN, Background: BLACK
|
||||
curses.init_pair(9, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: CYAN
|
||||
curses.init_pair(10, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
# Foreground: BLUE, Background: BLACK
|
||||
curses.init_pair(11, curses.COLOR_BLUE, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: BLUE
|
||||
curses.init_pair(12, curses.COLOR_BLACK, curses.COLOR_BLUE)
|
||||
# Foreground: MAGENTA, Background: BLACK
|
||||
curses.init_pair(13, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
||||
# Foreground: BLACK, Background: MAGENTA
|
||||
curses.init_pair(14, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
|
@ -1,73 +0,0 @@
|
||||
import curses
|
||||
|
||||
from core import utils
|
||||
|
||||
|
||||
class StatusBar:
|
||||
def __init__(self, instance):
|
||||
self.mode = instance.mode.upper()
|
||||
self.file = instance.buffer.name or "[No Name]"
|
||||
self.icon = instance.config["icon"] or "λ"
|
||||
self.theme = "default"
|
||||
self.colors = [7, 5, 13]
|
||||
self.components = [self.icon, self.mode, self.file]
|
||||
|
||||
def update(self, instance):
|
||||
self.mode = instance.mode.upper()
|
||||
self.components = [self.icon, self.mode, self.file]
|
||||
|
||||
def render(self, instance):
|
||||
# Clear the status bar
|
||||
utils.clear_line(instance, instance.height - 2, 0)
|
||||
|
||||
# Update variables
|
||||
self.update(instance)
|
||||
|
||||
if self.theme == "inverted":
|
||||
# Initialise the x position for each component
|
||||
x = 1
|
||||
|
||||
# Render each component
|
||||
for count, component in enumerate(self.components):
|
||||
instance.screen.addstr(instance.height - 2, x, component,
|
||||
curses.color_pair(self.colors[count]) | curses.A_BOLD)
|
||||
x += len(component) + 1
|
||||
|
||||
else:
|
||||
# Initialise temporary colors for inverted theme
|
||||
colors = []
|
||||
|
||||
# Add 1 to each color temporarily
|
||||
for color in self.colors:
|
||||
colors.append(color + 1)
|
||||
|
||||
# Initialise the x position for each component
|
||||
x = 0
|
||||
|
||||
# Render each component
|
||||
for count, component in enumerate(self.components):
|
||||
component = f" {component} "
|
||||
instance.screen.addstr(instance.height - 2, x, component,
|
||||
curses.color_pair(colors[count]) | curses.A_BOLD)
|
||||
x += len(component)
|
||||
|
||||
# Add a space at the end of the status bar
|
||||
instance.screen.addstr(instance.height - 2, x, " " * (instance.width - x),
|
||||
curses.color_pair(2))
|
||||
|
||||
|
||||
class Components:
|
||||
def __init__(self, instance, components: dict = None):
|
||||
self.components = components or {
|
||||
"left": [" "],
|
||||
"bottom": [StatusBar(instance)],
|
||||
}
|
||||
curses.endwin()
|
||||
|
||||
@staticmethod
|
||||
def get_component_width(component: list) -> int:
|
||||
return len(max(component))
|
||||
|
||||
def render(self, instance):
|
||||
for component in self.components["bottom"]:
|
||||
component.render(instance)
|
@ -1,68 +0,0 @@
|
||||
import curses
|
||||
|
||||
|
||||
def mode(to_mode: str):
|
||||
if to_mode == "block":
|
||||
print("\033[2 q")
|
||||
|
||||
elif to_mode == "line":
|
||||
print("\033[6 q")
|
||||
|
||||
elif to_mode == "hidden":
|
||||
curses.curs_set(0)
|
||||
|
||||
elif to_mode == "visible":
|
||||
curses.curs_set(1)
|
||||
|
||||
|
||||
def push(instance, direction: (int, str)):
|
||||
if direction in (0, "up", "north"):
|
||||
# If the cursor isn't at the top of the screen
|
||||
if instance.raw_cursor[0] > 0:
|
||||
# Move the cursor up
|
||||
instance.raw_cursor[0] -= 1
|
||||
|
||||
# Move the buffer upwards if the cursor is at the top of the screen and not at the top of the buffer
|
||||
if instance.raw_cursor[0] == 0 and instance.cursor[0] == instance.offset[0] and instance.cursor[0] != 0:
|
||||
instance.offset[0] -= 1
|
||||
|
||||
elif direction in (2, "down", "south"):
|
||||
if instance.raw_cursor[0] == instance.safe_height and instance.cursor[0] != len(instance.buffer.data) - 1:
|
||||
instance.offset[0] += 1
|
||||
|
||||
# If the cursor isn't at the bottom of the screen
|
||||
elif instance.raw_cursor[0] != instance.safe_height and instance.cursor[0] != len(instance.buffer.data) - 1:
|
||||
# Move the cursor down
|
||||
instance.raw_cursor[0] += 1
|
||||
|
||||
elif direction in (1, "right", "east"):
|
||||
# Move the cursor one to the right
|
||||
instance.raw_cursor[1] += 1
|
||||
|
||||
elif direction in (3, "left", "west"):
|
||||
# Move the cursor one to the left
|
||||
instance.raw_cursor[1] -= 1
|
||||
|
||||
|
||||
def check(instance, cursor: list) -> list:
|
||||
# Prevent the cursor from going outside the buffer
|
||||
cursor[1] = min(len(instance.buffer.data[instance.cursor[0]]) - 1, cursor[1])
|
||||
|
||||
# Prevent any negative values
|
||||
cursor[0] = max(0, cursor[0])
|
||||
cursor[1] = max(0, cursor[1])
|
||||
|
||||
# Prevent the cursor from going outside the screen
|
||||
cursor[1] = min(instance.safe_width, cursor[1])
|
||||
cursor[0] = min(instance.safe_height, cursor[0])
|
||||
|
||||
return cursor
|
||||
|
||||
|
||||
def move(instance):
|
||||
# Run a final check to see if the cursor is valid
|
||||
instance.raw_cursor = check(instance, instance.raw_cursor)
|
||||
|
||||
# Moves the cursor to anywhere on the screen
|
||||
instance.screen.move(instance.raw_cursor[0], instance.raw_cursor[1] +
|
||||
instance.components.get_component_width(instance.components.components["left"]))
|
@ -1,34 +0,0 @@
|
||||
from mode import normal, insert, command
|
||||
|
||||
|
||||
def activate(instance, mode):
|
||||
# Visibly update the mode
|
||||
instance.mode = mode
|
||||
|
||||
# Refresh the screen
|
||||
instance.refresh()
|
||||
|
||||
if mode == "command":
|
||||
# Activate command mode
|
||||
instance.components.components["bottom"][0].colors[1] = 5
|
||||
command.activate(instance)
|
||||
|
||||
elif mode == "insert":
|
||||
# Activate insert mode
|
||||
instance.components.components["bottom"][0].colors[1] = 11
|
||||
insert.activate()
|
||||
|
||||
elif mode == "normal":
|
||||
# Activate normal mode
|
||||
instance.components.components["bottom"][0].colors[1] = 5
|
||||
normal.activate()
|
||||
|
||||
|
||||
def handle_key(instance, key):
|
||||
# Normal mode - default keybindings
|
||||
if instance.mode == "normal":
|
||||
normal.execute(instance, key)
|
||||
|
||||
# Insert mode - inserting text to the buffer
|
||||
elif instance.mode == "insert":
|
||||
insert.execute(instance, key)
|
209
core/utils.py
209
core/utils.py
@ -1,209 +0,0 @@
|
||||
import curses
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from core import cursors
|
||||
from core.colors import Codes as Col
|
||||
|
||||
|
||||
def gracefully_exit():
|
||||
# Close the curses window
|
||||
curses.endwin()
|
||||
|
||||
# Finally, exit the program
|
||||
sys.exit()
|
||||
|
||||
|
||||
def clear_line(instance, y: int, x: int):
|
||||
# Clear the line at the screen at position y, x
|
||||
instance.screen.insstr(y, x, " " * (instance.width - x))
|
||||
|
||||
|
||||
def pause_screen(message: str):
|
||||
# End the curses session
|
||||
curses.endwin()
|
||||
|
||||
# Print the message and wait for enter key
|
||||
input(f"{message}\n\n Press enter to continue...")
|
||||
|
||||
|
||||
def load_file(file_path: str) -> dict:
|
||||
# load the json file with read permissions
|
||||
with open(file_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_file(instance, file_path: str, data: list):
|
||||
# Save the data to the file
|
||||
with open(file_path, "w") as f:
|
||||
try:
|
||||
# For each line in the file
|
||||
for index, line in enumerate(data):
|
||||
if index == len(data) - 1:
|
||||
# If this is the last line, write it without a newline
|
||||
f.write(line)
|
||||
|
||||
else:
|
||||
# Otherwise, write the line with a newline
|
||||
f.write(f"{line}\n")
|
||||
|
||||
except (OSError, IOError):
|
||||
# If the file could not be written, show an error message
|
||||
error(instance, f"File {file_path} could not be saved.")
|
||||
|
||||
|
||||
def load_config_file() -> dict:
|
||||
# Parse the path of the config file
|
||||
config_file_path = f"{Path.home()}/.config/lambda/config.json"
|
||||
|
||||
# Only if the config file exists, attempt to load it
|
||||
if os.path.exists(config_file_path):
|
||||
# Return the loaded config
|
||||
return load_file(config_file_path)
|
||||
|
||||
|
||||
def welcome(instance):
|
||||
# Startup text
|
||||
title = "λ Lambda"
|
||||
subtext = [
|
||||
"Next generation hackable text editor for nerds",
|
||||
"",
|
||||
"Type :h to open the README.md document",
|
||||
"Type :o <file> to open a file and edit",
|
||||
"Type :q or <C-c> to quit lambda.py"
|
||||
]
|
||||
|
||||
# Centering calculations
|
||||
start_x_title = int((instance.safe_width // 2) - (len(title) // 2) - len(title) % 2 + 2)
|
||||
start_y = int((instance.safe_height // 2) - 1)
|
||||
|
||||
# Rendering title
|
||||
instance.screen.addstr(start_y, start_x_title, title, curses.color_pair(7) | curses.A_BOLD)
|
||||
|
||||
# Print the subtext
|
||||
for text in subtext:
|
||||
start_y += 1
|
||||
start_x = int((instance.safe_width // 2) - (len(text) // 2) - len(text) % 2 + 2)
|
||||
instance.screen.addstr(start_y, start_x, text)
|
||||
|
||||
|
||||
def prompt(instance, message: str, color: int = 1) -> (list, None):
|
||||
# Initialise the input list
|
||||
inp = []
|
||||
|
||||
# Write whitespace over characters to refresh it
|
||||
clear_line(instance, instance.height - 1, len(message) + len(inp) - 1)
|
||||
|
||||
# Write the message to the screen
|
||||
instance.screen.addstr(instance.height - 1, 0, message, curses.color_pair(color))
|
||||
|
||||
while True:
|
||||
# Wait for a keypress
|
||||
key = instance.screen.getch()
|
||||
|
||||
# Subtracting a key (backspace)
|
||||
if key in (curses.KEY_BACKSPACE, 127, '\b'):
|
||||
# Write whitespace over characters to refresh it
|
||||
clear_line(instance, instance.height - 1, len(message) + len(inp) - 1)
|
||||
|
||||
if inp:
|
||||
# Subtract a character from the input list
|
||||
inp.pop()
|
||||
|
||||
else:
|
||||
# Exit the prompt without returning the input
|
||||
return None
|
||||
|
||||
elif key == 27:
|
||||
# Exit the prompt, without returning the input
|
||||
return None
|
||||
|
||||
elif key in (curses.KEY_ENTER, ord('\n'), ord('\r'), ord(":"), ord(";")):
|
||||
# Return the input list
|
||||
return inp
|
||||
|
||||
else:
|
||||
# If any other key is typed, append it
|
||||
# As long as the key is in the valid list
|
||||
valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!/_-0123456789 "
|
||||
if chr(key) in valid and len(inp) < (instance.width - 2):
|
||||
inp.append(chr(key))
|
||||
|
||||
# Refresh the screen
|
||||
instance.refresh()
|
||||
|
||||
# Write the message to the screen
|
||||
instance.screen.addstr(instance.height - 1, 0, message, curses.color_pair(color))
|
||||
|
||||
# Join the input together for visibility on the screen
|
||||
input_text = "".join(inp)
|
||||
|
||||
# Write the input text to the screen
|
||||
instance.screen.addstr(instance.height - 1, len(message), input_text)
|
||||
|
||||
|
||||
def press_key_to_continue(instance, message: str, color: int = 1):
|
||||
# Hide the cursor
|
||||
cursors.mode("hidden")
|
||||
|
||||
# Clear the bottom of the screen
|
||||
clear_line(instance, instance.height - 1, 0)
|
||||
|
||||
# Write the entire message to the screen
|
||||
instance.screen.addstr(instance.height - 1, 0, message, curses.color_pair(color))
|
||||
instance.screen.addstr(instance.height - 1, len(message) + 1, f"(press any key)")
|
||||
|
||||
# Wait for a keypress
|
||||
instance.screen.getch()
|
||||
|
||||
# Clear the bottom of the screen
|
||||
clear_line(instance, instance.height - 1, 0)
|
||||
|
||||
# Show the cursor
|
||||
cursors.mode("visible")
|
||||
|
||||
|
||||
def error(instance, message: str):
|
||||
# Parse the error message
|
||||
error_message = f"ERROR: {message}"
|
||||
|
||||
# Create a prompt
|
||||
press_key_to_continue(instance, error_message, 3)
|
||||
|
||||
|
||||
def fatal_error(exception: Exception):
|
||||
# End the curses session
|
||||
curses.endwin()
|
||||
|
||||
# Print the error message and traceback
|
||||
print(f"{Col.red}FATAL ERROR:{Col.end} "
|
||||
f"{Col.yellow}{exception}{Col.end}\n")
|
||||
print(traceback.format_exc())
|
||||
|
||||
# Exit, with an error exit code
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def goodbye(instance):
|
||||
try:
|
||||
# Confirm before exiting
|
||||
choice = prompt(instance, "Really quit lambda? (y/n): ", 11)
|
||||
|
||||
# If the user confirms, exit
|
||||
if choice and choice[0] == "y":
|
||||
gracefully_exit()
|
||||
|
||||
# Clear the prompt if the user cancels
|
||||
else:
|
||||
clear_line(instance, instance.height - 1, 0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# If the user presses Ctrl+C, just exit
|
||||
gracefully_exit()
|
||||
|
||||
except Exception as exception:
|
||||
# If there is an error, print the error message and traceback
|
||||
fatal_error(exception)
|
11
install.sh
11
install.sh
@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copy lambda to ~/.local/share
|
||||
mkdir -p ~/.local/share/lambda
|
||||
cp -rf . ~/.local/share/lambda
|
||||
|
||||
# Copy lambda launcher
|
||||
chmod +x ./lambda
|
||||
rm -rf ~/.local/bin/lambda
|
||||
ln -s ~/.local/share/lambda/lambda ~/.local/bin/lambda
|
||||
chmod +x ~/.local/bin/lambda
|
126
main.py
126
main.py
@ -1,126 +0,0 @@
|
||||
import argparse
|
||||
import curses
|
||||
import os
|
||||
|
||||
from core import colors, cursors, buffers, modes, utils
|
||||
from core.buffers import Buffer
|
||||
from core.components import Components
|
||||
|
||||
|
||||
class Lambda:
|
||||
def __init__(self, buffer: Buffer = None, config: dict = None):
|
||||
self.cursor = [0, 0]
|
||||
self.raw_cursor = [0, 0]
|
||||
self.offset = [0, 0]
|
||||
self.height = 0
|
||||
self.width = 0
|
||||
self.safe_height = 0
|
||||
self.safe_width = 0
|
||||
self.mode = "normal"
|
||||
self.config = config or {"icon": "λ"}
|
||||
self.buffer = buffer or [""]
|
||||
self.screen = curses.initscr()
|
||||
self.components = Components(self)
|
||||
|
||||
def update_dimensions(self):
|
||||
# Calculate the entire height and width of the terminal
|
||||
self.height, self.width = self.screen.getmaxyx()
|
||||
|
||||
# Calculate the safe area for the buffer by removing heights & widths of components
|
||||
self.safe_height = self.height - len(self.components.components["bottom"]) - 2
|
||||
self.safe_width = self.width - self.components.get_component_width(self.components.components["left"]) - 1
|
||||
|
||||
def refresh(self):
|
||||
# Calculate the cursor position in the file
|
||||
self.cursor[0], self.cursor[1] = self.raw_cursor[0] + self.offset[0], self.raw_cursor[1] + self.offset[1]
|
||||
|
||||
# Update the dimensions of the terminal
|
||||
self.update_dimensions()
|
||||
|
||||
# Write the buffer to the screen
|
||||
self.buffer.render(self)
|
||||
|
||||
# Refresh the on-screen components
|
||||
self.components.render(self)
|
||||
|
||||
# Move the cursor
|
||||
cursors.move(self)
|
||||
|
||||
def start(self):
|
||||
# Change the escape key delay to 25ms
|
||||
# Fixes an issue where the "esc" key takes way too long to press
|
||||
os.environ.setdefault("ESCDELAY", "25")
|
||||
|
||||
# Initialise colors
|
||||
colors.init_colors()
|
||||
|
||||
# Change the cursor shape
|
||||
cursors.mode("block")
|
||||
|
||||
# Don't echo any key-presses
|
||||
curses.noecho()
|
||||
|
||||
# Show a welcome message if lambda opens with no file
|
||||
if not self.buffer.path:
|
||||
# Update the screen variables
|
||||
self.refresh()
|
||||
|
||||
# Show the welcome message
|
||||
utils.welcome(self)
|
||||
|
||||
# Main loop
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# The main loop, which runs until the user quits
|
||||
while True:
|
||||
# Update the screen variables
|
||||
self.refresh()
|
||||
|
||||
# Wait for a keypress
|
||||
key = self.screen.getch()
|
||||
|
||||
# Handle the key
|
||||
modes.handle_key(self, key)
|
||||
|
||||
# Refresh and clear the screen
|
||||
self.screen.erase()
|
||||
|
||||
except KeyboardInterrupt: # Ctrl-C
|
||||
# Create a goodbye prompt
|
||||
utils.goodbye(self)
|
||||
|
||||
# Run the main loop again
|
||||
self.run()
|
||||
|
||||
|
||||
def main():
|
||||
# Shell arguments
|
||||
parser = argparse.ArgumentParser(description="Next generation hackable text editor for nerds.")
|
||||
parser.add_argument("file", metavar="file", type=str, nargs="?",
|
||||
help="The name of a file for lambda to open")
|
||||
|
||||
# Collect the arguments passed into lambda at the shell
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load the file into a Buffer object
|
||||
buffer = buffers.load_file(args.file)
|
||||
|
||||
# Load the config
|
||||
config = utils.load_config_file()
|
||||
|
||||
# Load lambda with the buffer object
|
||||
instance = Lambda(buffer, config)
|
||||
|
||||
# Start the screen, this will loop until exit
|
||||
try:
|
||||
instance.start()
|
||||
|
||||
# Excepts *any* errors that occur
|
||||
except Exception as exception:
|
||||
utils.fatal_error(exception)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,41 +0,0 @@
|
||||
from core import utils
|
||||
|
||||
|
||||
def execute(instance, commands: list):
|
||||
# Only if commands are given
|
||||
if commands:
|
||||
# Check each command in the list of commands
|
||||
for command in commands:
|
||||
if command == "d": # Debug
|
||||
# Create the debug prompt
|
||||
utils.press_key_to_continue(instance, f"Cursor: {instance.cursor} Raw: {instance.raw_cursor} "
|
||||
f"Len: {len(instance.buffer.data)}")
|
||||
|
||||
elif command == "t": # Toggle
|
||||
# Toggle the status bar theme
|
||||
if instance.components.components["bottom"][0].theme == "default":
|
||||
instance.components.components["bottom"][0].theme = "inverted"
|
||||
else:
|
||||
instance.components.components["bottom"][0].theme = "default"
|
||||
|
||||
elif command == "w": # Write
|
||||
# Write to the file
|
||||
utils.save_file(instance, instance.buffer.path, instance.buffer.data)
|
||||
|
||||
elif command == "q": # Quit
|
||||
# Create a goodbye prompt
|
||||
utils.goodbye(instance)
|
||||
|
||||
else: # Invalid command
|
||||
utils.error(instance, f"invalid command: '{command}'")
|
||||
|
||||
|
||||
def activate(instance):
|
||||
# Create a prompt, which returns the input (commands)
|
||||
commands = utils.prompt(instance, ":")
|
||||
|
||||
# Execute the commands given
|
||||
execute(instance, commands)
|
||||
|
||||
# Return to normal mode once all commands are executed
|
||||
instance.mode = "normal"
|
@ -1,29 +0,0 @@
|
||||
import curses
|
||||
|
||||
from core import cursors, modes
|
||||
|
||||
|
||||
def execute(instance, key):
|
||||
if key == 27: # Escape
|
||||
# Switch to normal mode
|
||||
modes.activate(instance, "normal")
|
||||
|
||||
elif key in (curses.KEY_BACKSPACE, 127, '\b'): # Backspace
|
||||
if instance.cursor[1] > 0:
|
||||
# Delete the character before the cursor
|
||||
instance.buffer.delete_char(instance)
|
||||
|
||||
# Move the cursor one to the left
|
||||
cursors.push(instance, 3)
|
||||
|
||||
else:
|
||||
# Insert the character
|
||||
instance.buffer.insert_char(instance, chr(key))
|
||||
|
||||
# Move the cursor one to the right
|
||||
cursors.push(instance, 1)
|
||||
|
||||
|
||||
def activate():
|
||||
# Switch the cursor to a line
|
||||
cursors.mode("line")
|
@ -1,45 +0,0 @@
|
||||
import curses
|
||||
|
||||
from core import cursors, modes, utils
|
||||
|
||||
|
||||
def execute(instance, key):
|
||||
if key == curses.BUTTON1_CLICKED:
|
||||
# Move the cursor to the position clicked
|
||||
utils.prompt(instance, str(curses.getmouse()))
|
||||
|
||||
elif key in (ord("j"), curses.KEY_DOWN):
|
||||
# Move the cursor down
|
||||
cursors.push(instance, "down")
|
||||
|
||||
elif key in (ord("k"), curses.KEY_UP):
|
||||
# Move the cursor up
|
||||
cursors.push(instance, "up")
|
||||
|
||||
elif key in (ord("l"), curses.KEY_RIGHT):
|
||||
# Move the cursor right
|
||||
cursors.push(instance, "right")
|
||||
|
||||
elif key in (ord("h"), curses.KEY_LEFT):
|
||||
# Move the cursor left
|
||||
cursors.push(instance, "left")
|
||||
|
||||
elif key == ord("i"):
|
||||
# Activate insert mode
|
||||
modes.activate(instance, "insert")
|
||||
|
||||
elif key == ord("I"):
|
||||
# Move the cursor to the right
|
||||
cursors.push(instance, "right")
|
||||
|
||||
# Then activate insert mode
|
||||
modes.activate(instance, "insert")
|
||||
|
||||
elif key in (ord(":"), ord(";")):
|
||||
# Activate command mode
|
||||
modes.activate(instance, "command")
|
||||
|
||||
|
||||
def activate():
|
||||
# Switch the cursor to a block
|
||||
cursors.mode("block")
|
60
src/editor.rs
Normal file
60
src/editor.rs
Normal file
@ -0,0 +1,60 @@
|
||||
pub struct Config<'a> {
|
||||
pub logo: &'a str,
|
||||
pub friendly_name: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Config<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
logo: "λ",
|
||||
friendly_name: "Lambda",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Buffer<'a> {
|
||||
pub data: Vec<String>,
|
||||
pub name: &'a str,
|
||||
pub path: &'a str,
|
||||
}
|
||||
|
||||
#[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: Buffer<'a>,
|
||||
pub cursors: Vec<i32>,
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
impl<'a> Editor<'a> {
|
||||
pub fn new() -> Self {
|
||||
Editor {
|
||||
config: Config::new(),
|
||||
buffer: Buffer {
|
||||
data: Vec::from([String::from("Hello"), String::from("World")]),
|
||||
name: "[No Name]",
|
||||
path: "/home/spy",
|
||||
},
|
||||
cursors: Vec::from([0]),
|
||||
mode: Mode::Normal,
|
||||
}
|
||||
}
|
||||
}
|
9
src/main.rs
Normal file
9
src/main.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod editor;
|
||||
mod terminal;
|
||||
mod tui;
|
||||
|
||||
fn main() {
|
||||
let lambda = editor::Editor::new();
|
||||
let mut screen = terminal::Screen::new().unwrap();
|
||||
tui::start(&mut screen, lambda);
|
||||
}
|
154
src/terminal.rs
Normal file
154
src/terminal.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);
|
||||
}
|
||||
}
|
131
src/tui.rs
Normal file
131
src/tui.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use crate::editor::Editor;
|
||||
use crate::terminal::{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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user