rust branch

This commit is contained in:
Maddie H 2022-11-03 07:31:24 +00:00
parent 9126abc6d5
commit ca0938b523
No known key found for this signature in database
GPG Key ID: 64FAA9959751687D
18 changed files with 38 additions and 820 deletions

4
.gitignore vendored
View File

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

7
Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "lambda"
version = "0.1.0"

8
Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "lambda"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -1,35 +0,0 @@
# λ lambda
Next generation hackable text editor for nerds.
### 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
```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
```

View File

@ -1 +0,0 @@
## TODO

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

21
src/main.rs Normal file
View File

@ -0,0 +1,21 @@
struct Buffer {
data: Vec<String>,
name: &str,
}
struct Editor {
buffer: Buffer,
}
fn main() {
let bufffer = Buffer {
data: Vec::from([String::from("a test")]),
name: "uhh"
};
let editor = Editor {
buffer: bufffer,
};
println!("{}", editor.buffer.data[0])
}