commit ddf99d71f69fe899f448c954409beed65d914a78 Author: spy Date: Mon Mar 14 07:21:55 2022 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..28bfdec --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/lambda.iml b/.idea/lambda.iml new file mode 100644 index 0000000..bd25c05 --- /dev/null +++ b/.idea/lambda.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..dc9ea49 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ea736d6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c53b90f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.extraPaths": [ + "./modes" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d69472b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# lambda diff --git a/__pycache__/colour.cpython-310.pyc b/__pycache__/colour.cpython-310.pyc new file mode 100644 index 0000000..db72e4b Binary files /dev/null and b/__pycache__/colour.cpython-310.pyc differ diff --git a/core/__pycache__/colors.cpython-310.pyc b/core/__pycache__/colors.cpython-310.pyc new file mode 100644 index 0000000..7e005b8 Binary files /dev/null and b/core/__pycache__/colors.cpython-310.pyc differ diff --git a/core/__pycache__/cursor.cpython-310.pyc b/core/__pycache__/cursor.cpython-310.pyc new file mode 100644 index 0000000..634fb05 Binary files /dev/null and b/core/__pycache__/cursor.cpython-310.pyc differ diff --git a/core/__pycache__/statusbar.cpython-310.pyc b/core/__pycache__/statusbar.cpython-310.pyc new file mode 100644 index 0000000..bbb0bdd Binary files /dev/null and b/core/__pycache__/statusbar.cpython-310.pyc differ diff --git a/core/__pycache__/utils.cpython-310.pyc b/core/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..5950a7f Binary files /dev/null and b/core/__pycache__/utils.cpython-310.pyc differ diff --git a/core/colors.py b/core/colors.py new file mode 100644 index 0000000..cfea4b2 --- /dev/null +++ b/core/colors.py @@ -0,0 +1,20 @@ +import curses + + +def init_colors(): + # Start colors in curses + curses.start_color() + curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE) + curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_RED) + curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_GREEN) + curses.init_pair(7, curses.COLOR_YELLOW, curses.COLOR_BLACK) + curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_YELLOW) + curses.init_pair(9, curses.COLOR_CYAN, curses.COLOR_BLACK) + curses.init_pair(10, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(11, curses.COLOR_BLUE, curses.COLOR_BLACK) + curses.init_pair(12, curses.COLOR_BLACK, curses.COLOR_BLUE) + curses.init_pair(13, curses.COLOR_MAGENTA, curses.COLOR_BLACK) + curses.init_pair(14, curses.COLOR_BLACK, curses.COLOR_MAGENTA) diff --git a/core/cursor.py b/core/cursor.py new file mode 100644 index 0000000..c2ee144 --- /dev/null +++ b/core/cursor.py @@ -0,0 +1,5 @@ +def cursor_mode(mode): + if mode == "block": + print("\033[2 q") + elif mode == "line": + print("\033[6 q") \ No newline at end of file diff --git a/core/statusbar.py b/core/statusbar.py new file mode 100644 index 0000000..79d95a5 --- /dev/null +++ b/core/statusbar.py @@ -0,0 +1,35 @@ +import curses + + +def refresh(stdscr, height, width, mode, colors=None, theme="inverted", icon="λ", file="[No Name]"): + if colors is None: + colors = [8, 6, 14, 2] + + if theme == "inverted": + # Add spaces on either end + icon = f" {icon} " + mode = f" {mode} " + file = f" {file} " + + else: + # Subtract one from all colours + for index, color in enumerate(colors): + colors[index] -= 1 + + # Add spaces before each part + icon = f" {icon}" + mode = f" {mode}" + file = f" {file}" + + # Render icon + stdscr.addstr(height - 2, 0, icon, curses.color_pair(colors[0]) | curses.A_BOLD) + + # Render mode + stdscr.addstr(height - 2, len(icon), mode, curses.color_pair(colors[1]) | curses.A_BOLD) + + # Render file name + stdscr.addstr(height - 2, len(icon) + len(mode), file, curses.color_pair(colors[2]) | curses.A_BOLD) + + # Rest of the bar + stdscr.addstr(height - 2, len(icon) + len(mode) + len(file), " " * (width - (len(icon) + len(mode) + len(file))), + curses.color_pair(colors[3])) diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..b3c3b2f --- /dev/null +++ b/core/utils.py @@ -0,0 +1,28 @@ +from sys import exit +import curses + + +def goodbye(stdscr, height, width): + # The prompt message + prompt = "Really quit lambda? (y or n): " + + # Clear the bottom line + stdscr.addstr(height-1, 0, " " * (width - 1), curses.color_pair(1)) + + # Print the prompt + stdscr.addstr(height-1, 0, prompt, curses.color_pair(11)) + + # Wait for and capture a key press from the user + key = stdscr.getch() + + if key == ord("y"): + # Only exit if the key was "y", a confirmation + exit() + + # Clear the bottom line again + stdscr.addstr(height-1, 0, " " * (width - 1), curses.color_pair(1)) + + +def error(stdscr, height, error_msg): + error_msg = f" ERROR {error_msg}" + stdscr.addstr(height-1, 0, error_msg, curses.color_pair(3)) diff --git a/lambda b/lambda new file mode 100755 index 0000000..96c6e2c --- /dev/null +++ b/lambda @@ -0,0 +1,85 @@ +#!/usr/bin/python +import os +import curses +from core import colors, cursor +from modes import normal + + +def start_screen(stdscr): + # Get window height and width + height, width = stdscr.getmaxyx() + + # Startup text + title = "λ Lambda" + subtext = [ + "A performant, efficient and hackable text editor", + "", + "Type :h to open the README.md document", + "Type :o to open a file and edit", + "Type :q or to quit lambda" + ] + + # Centering calculations + start_x_title = int((width // 2) - (len(title) // 2) - len(title) % 2) + start_y = int((height // 2) - 2) + + # Rendering title + stdscr.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((width // 2) - (len(text) // 2) - len(text) % 2) + stdscr.addstr(start_y, start_x, text) + + +def start(stdscr): + # Initialise data before starting + data = {"cursor_y": 0, "cursor_x": 0, "commands": []} + + # Clear and refresh the screen for a blank canvas + stdscr.clear() + stdscr.refresh() + + # Initialise colors + colors.init_colors() + + # Load the start screen + start_screen(stdscr) + + # Change the cursor shape + cursor.cursor_mode("block") + + # Main loop + while True: + # Get the height and width of the screen + height, width = stdscr.getmaxyx() + + # Activate normal mode + data = normal.activate(stdscr, height, width, data) + + # Calculate a valid cursor position from data + cursor_x = max(2, data["cursor_x"]) + cursor_x = min(width - 1, cursor_x) + cursor_y = max(0, data["cursor_y"]) + cursor_y = min(height - 3, cursor_y) + + # Move the cursor + stdscr.move(cursor_y, cursor_x) + + # Refresh and clear the screen + stdscr.refresh() + stdscr.clear() + + +def main(): + # Change the escape delay to 25ms + # Fixes an issue where esc takes too long to press + os.environ.setdefault("ESCDELAY", "25") + + # Initialise the screen + curses.wrapper(start) + + +if __name__ == "__main__": + main() diff --git a/modes/__pycache__/command.cpython-310.pyc b/modes/__pycache__/command.cpython-310.pyc new file mode 100644 index 0000000..0fb2c77 Binary files /dev/null and b/modes/__pycache__/command.cpython-310.pyc differ diff --git a/modes/__pycache__/command_mode.cpython-310.pyc b/modes/__pycache__/command_mode.cpython-310.pyc new file mode 100644 index 0000000..b2ba55f Binary files /dev/null and b/modes/__pycache__/command_mode.cpython-310.pyc differ diff --git a/modes/__pycache__/insert.cpython-310.pyc b/modes/__pycache__/insert.cpython-310.pyc new file mode 100644 index 0000000..a607828 Binary files /dev/null and b/modes/__pycache__/insert.cpython-310.pyc differ diff --git a/modes/__pycache__/normal.cpython-310.pyc b/modes/__pycache__/normal.cpython-310.pyc new file mode 100644 index 0000000..9cae3d9 Binary files /dev/null and b/modes/__pycache__/normal.cpython-310.pyc differ diff --git a/modes/command.py b/modes/command.py new file mode 100644 index 0000000..751c7c8 --- /dev/null +++ b/modes/command.py @@ -0,0 +1,71 @@ +from core import statusbar, utils +import curses + + +def execute(stdscr, height, width, commands): + if not commands: + # Quit if there are no commands, don't check anything + return + + for command in commands: + if command == "w": + # Write to the file + pass + elif command == "q": + # Goodbye prompt + utils.goodbye(stdscr, height, width) + + +def activate(stdscr, height, width, data): + # Initialise variables + key = 0 + commands = [] + + # Visibly switch to command mode + statusbar.refresh(stdscr, height, width, "COMMAND") + stdscr.addstr(height-1, 0, ":") + stdscr.move(height-1, 1) + + # Main loop + while True: + # Get a key inputted by the user + key = stdscr.getch() + + # Handle subtracting a key (backspace) + if key == curses.KEY_BACKSPACE: + # Write whitespace over characters to refresh it + stdscr.addstr(height-1, 1, " " * len(commands)) + + if commands: + # Subtract a character + commands.pop() + else: + # If there's nothing left, quit the loop + return data + + elif key == 27: + # Quit the loop if escape is pressed + return data + + elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')): + # Execute commands + execute(stdscr, height, width, commands) + + # Clear the bottom bar + stdscr.addstr(height - 1, 0, " " * (width - 1)) + + return data + + else: + # If any other key is typed, append it + # As long as the key is in the valid list + valid = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ!" + if chr(key) in valid and len(commands) < (width - 2): + commands.append(chr(key)) + + # Join the commands together for visibility on the screen + friendly_command = "".join(commands) + # Write the commands to the screen + stdscr.addstr(height-1, 1, friendly_command) + # Move the cursor the end of the commands + stdscr.move(height-1, len(commands)+1) diff --git a/modes/insert.py b/modes/insert.py new file mode 100644 index 0000000..9641028 --- /dev/null +++ b/modes/insert.py @@ -0,0 +1,17 @@ +from core import statusbar + + +def execute(stdscr, height, width, key): + return + + +def activate(stdscr, height, width, data): + # Refresh the status bar with a different colour for insert + colors = [8, 12, 14, 2] + statusbar.refresh(stdscr, height, width, "INSERT", colors) + + while True: + # Wait for and capture a key press from the user + key = stdscr.getch() + + return data diff --git a/modes/normal.py b/modes/normal.py new file mode 100644 index 0000000..cbd4893 --- /dev/null +++ b/modes/normal.py @@ -0,0 +1,43 @@ +from modes import command +from modes import insert +from core import statusbar + + +def execute(stdscr, height, width, key, data): + if key == ord("j"): + # Move the cursor down + data["cursor_y"] += 1 + + elif key == ord("k"): + # Move the cursor up + data["cursor_y"] -= 1 + + elif key == ord("l"): + # Move the cursor right + data["cursor_x"] += 1 + + elif key == ord("h"): + # Move the cursor left + data["cursor_x"] -= 1 + + elif key == ord("i"): + # Insert mode + data = insert.activate(stdscr, height, width, data) + + elif key in (ord(":"), ord(";")): + # Switch to command mode + data = command.activate(stdscr, height, width, data) + + return data + + +def activate(stdscr, height, width, data): + # Refresh the status bar + statusbar.refresh(stdscr, height, width, "NORMAL") + + # Wait for and capture a key press from the user + key = stdscr.getch() + + # Check against the keybindings + data = execute(stdscr, height, width, key, data) + return data