Merge pull request #1 from SpyHoodle/rust

rust rewrite
This commit is contained in:
Maddie 2022-11-07 20:19:19 +00:00 committed by GitHub
commit 8857c6a99f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 651 additions and 816 deletions

4
.gitignore vendored
View File

@ -1,4 +1,2 @@
__pycache__
core/__pycache__
mode/__pycache__
.idea
target

1
.replit Normal file
View File

@ -0,0 +1 @@
run = "cargo run"

249
Cargo.lock generated Normal file
View 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
View 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"

View File

@ -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!
```

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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"]))

View File

@ -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)

View File

@ -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)

View File

@ -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

3
lambda
View File

@ -1,3 +0,0 @@
#!/bin/sh
python3 ~/.local/share/lambda/main.py "$@"

126
main.py
View File

@ -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()

View File

@ -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"

View File

@ -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")

View File

@ -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
View 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
View 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
View 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
View 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();
}