diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c53b90f..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "python.analysis.extraPaths": [ - "./modes" - ] -} \ No newline at end of file diff --git a/core/__pycache__/cursor.cpython-310.pyc b/core/__pycache__/cursor.cpython-310.pyc index 634fb05..e726a47 100644 Binary files a/core/__pycache__/cursor.cpython-310.pyc and b/core/__pycache__/cursor.cpython-310.pyc differ diff --git a/core/__pycache__/mode.cpython-310.pyc b/core/__pycache__/mode.cpython-310.pyc new file mode 100644 index 0000000..27c7be4 Binary files /dev/null and b/core/__pycache__/mode.cpython-310.pyc differ diff --git a/core/__pycache__/statusbar.cpython-310.pyc b/core/__pycache__/statusbar.cpython-310.pyc index bbb0bdd..9335a90 100644 Binary files a/core/__pycache__/statusbar.cpython-310.pyc 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 index 5950a7f..e307307 100644 Binary files a/core/__pycache__/utils.cpython-310.pyc and b/core/__pycache__/utils.cpython-310.pyc differ diff --git a/core/buffer.py b/core/buffer.py new file mode 100644 index 0000000..e69de29 diff --git a/core/cursor.py b/core/cursor.py index c2ee144..c0b6069 100644 --- a/core/cursor.py +++ b/core/cursor.py @@ -1,5 +1,29 @@ def cursor_mode(mode): if mode == "block": print("\033[2 q") + elif mode == "line": - print("\033[6 q") \ No newline at end of file + print("\033[6 q") + + elif mode == "hidden": + print('\033[? 25l') + + elif mode == "visible": + print('\033[? 25h') + + +def check_cursor(data): + data["cursor_x"] = max(2, data["cursor_x"]) + data["cursor_x"] = min(data["width"] - 1, data["cursor_x"]) + data["cursor_y"] = max(0, data["cursor_y"]) + data["cursor_y"] = min(data["height"] - 3, data["cursor_y"]) + + return data + + +def move(stdscr, data): + # Calculate a valid cursor position from data + data = check_cursor(data) + + # Move the cursor + stdscr.move(data["cursor_y"], data["cursor_x"]) diff --git a/core/mode.py b/core/mode.py new file mode 100644 index 0000000..918464a --- /dev/null +++ b/core/mode.py @@ -0,0 +1,17 @@ +from modes import normal, insert, command + + +def activate(stdscr, data): + if data["mode"] == "normal": + data["mode_color"] = 6 + data = normal.activate(stdscr, data) + + elif data["mode"] == "insert": + data["mode_color"] = 12 + data = insert.activate(stdscr, data) + + elif data["mode"] == "command": + data["mode_color"] = 6 + data = command.activate(stdscr, data) + + return data diff --git a/core/statusbar.py b/core/statusbar.py index 79d95a5..7101e5e 100644 --- a/core/statusbar.py +++ b/core/statusbar.py @@ -1,35 +1,43 @@ 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 +def themes(data): + if data["statusbar_theme"] == "bare": + # The theme colors + colors = (7, data["mode_color"] - 1, 13, 1) # Add spaces before each part - icon = f" {icon}" - mode = f" {mode}" - file = f" {file}" + icon = f" {data['icon']}" + mode = f" {data['mode'].upper()}" + file = f" {data['file']}" + else: + # The theme colors + colors = (8, data["mode_color"], 14, 2) + + # Add spaces on either end + icon = f" {data['icon']} " + mode = f" {data['mode'].upper()} " + file = f" {data['file']} " + + return colors, icon, mode, file + + +def refresh(stdscr, data): + colors, icon, mode, file = themes(data) # Render icon - stdscr.addstr(height - 2, 0, icon, curses.color_pair(colors[0]) | curses.A_BOLD) + stdscr.addstr(data["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) + stdscr.addstr(data["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) + stdscr.addstr(data["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))), + stdscr.addstr(data["height"] - 2, len(icon) + len(mode) + len(file), + " " * (data["width"] - (len(icon) + len(mode) + len(file))), curses.color_pair(colors[3])) diff --git a/core/utils.py b/core/utils.py index b3c3b2f..8c604c6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,16 +1,17 @@ from sys import exit +from core import cursor import curses -def goodbye(stdscr, height, width): +def goodbye(stdscr, data): # 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)) + stdscr.addstr(data["height"]-1, 0, " " * (data["width"] - 1), curses.color_pair(1)) # Print the prompt - stdscr.addstr(height-1, 0, prompt, curses.color_pair(11)) + stdscr.addstr(data["height"]-1, 0, prompt, curses.color_pair(11)) # Wait for and capture a key press from the user key = stdscr.getch() @@ -20,9 +21,17 @@ def goodbye(stdscr, height, width): exit() # Clear the bottom line again - stdscr.addstr(height-1, 0, " " * (width - 1), curses.color_pair(1)) + stdscr.addstr(data["height"]-1, 0, " " * (data["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)) +def error(stdscr, data, error_msg): + # Print the error message to the bottom line + error_msg = f"ERROR: {error_msg}" + stdscr.addstr(data["height"]-1, 0, error_msg, curses.color_pair(3)) + stdscr.addstr(data["height"]-1, len(error_msg) + 1, "(press any key) ", curses.color_pair(1)) + + # Wait for a key to be pressed + stdscr.getch() + + # Clear the bottom line + stdscr.addstr(data["height"]-1, 0, " " * (data["width"] - 1), curses.color_pair(1)) \ No newline at end of file diff --git a/lambda b/lambda index 96c6e2c..f7c2b09 100755 --- a/lambda +++ b/lambda @@ -1,8 +1,8 @@ #!/usr/bin/python +from core import colors, cursor, mode import os import curses -from core import colors, cursor -from modes import normal +import argparse def start_screen(stdscr): @@ -12,7 +12,7 @@ def start_screen(stdscr): # Startup text title = "λ Lambda" subtext = [ - "A performant, efficient and hackable text editor", + "Next generation hackable text editor for nerds", "", "Type :h to open the README.md document", "Type :o to open a file and edit", @@ -33,39 +33,35 @@ def start_screen(stdscr): stdscr.addstr(start_y, start_x, text) -def start(stdscr): +def start(stdscr, file): # 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() + data = { + "cursor_y": 0, + "cursor_x": 0, + "height": 0, + "width": 0, + "mode": "normal", + "icon": "λ", + "file": file, + "statusbar_theme": "filled" + } # Initialise colors colors.init_colors() - # Load the start screen - start_screen(stdscr) - # Change the cursor shape cursor.cursor_mode("block") + # Start the screen + start_screen(stdscr) + # Main loop while True: # Get the height and width of the screen - height, width = stdscr.getmaxyx() + data["height"], data["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) + # Activate the next mode + data = mode.activate(stdscr, data) # Refresh and clear the screen stdscr.refresh() @@ -73,12 +69,24 @@ def start(stdscr): def main(): + parser = argparse.ArgumentParser(description="Process some integers.") + parser.add_argument("file", metavar="file", type=str, nargs="?", + help="File to open") + + args = parser.parse_args() + # Check the file name + if args.file: + file = args.file + + else: + file = "[No Name]" + # 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) + curses.wrapper(start, file) if __name__ == "__main__": diff --git a/modes/__pycache__/command.cpython-310.pyc b/modes/__pycache__/command.cpython-310.pyc index 0fb2c77..474b6b8 100644 Binary files a/modes/__pycache__/command.cpython-310.pyc and b/modes/__pycache__/command.cpython-310.pyc differ diff --git a/modes/__pycache__/insert.cpython-310.pyc b/modes/__pycache__/insert.cpython-310.pyc index a607828..80f41fa 100644 Binary files a/modes/__pycache__/insert.cpython-310.pyc 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 index 9cae3d9..3e8be88 100644 Binary files a/modes/__pycache__/normal.cpython-310.pyc and b/modes/__pycache__/normal.cpython-310.pyc differ diff --git a/modes/command.py b/modes/command.py index 751c7c8..0dd0c51 100644 --- a/modes/command.py +++ b/modes/command.py @@ -2,7 +2,7 @@ from core import statusbar, utils import curses -def execute(stdscr, height, width, commands): +def execute(stdscr, data, commands): if not commands: # Quit if there are no commands, don't check anything return @@ -11,20 +11,32 @@ def execute(stdscr, height, width, commands): if command == "w": # Write to the file pass + elif command == "q": # Goodbye prompt - utils.goodbye(stdscr, height, width) + utils.goodbye(stdscr, data) + + elif command == "t": + if data["statusbar_theme"] == "filled": + data["statusbar_theme"] = "bare" + + else: + data["statusbar_theme"] = "filled" + + else: + utils.error(stdscr, data, f"Not an editor command: '{command}'") + + return data -def activate(stdscr, height, width, data): +def activate(stdscr, 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) + statusbar.refresh(stdscr, data) + stdscr.addstr(data["height"]-1, 0, ":") + stdscr.move(data["height"]-1, 1) # Main loop while True: @@ -34,38 +46,44 @@ def activate(stdscr, height, width, data): # Handle subtracting a key (backspace) if key == curses.KEY_BACKSPACE: # Write whitespace over characters to refresh it - stdscr.addstr(height-1, 1, " " * len(commands)) + stdscr.addstr(data["height"]-1, 1, " " * len(commands)) if commands: # Subtract a character commands.pop() else: - # If there's nothing left, quit the loop + # Exit command mode and enter normal mode if there is nothing left + data["mode"] = "normal" return data elif key == 27: - # Quit the loop if escape is pressed + # Exit command mode and enter normal mode if "esc" is pressed + data["mode"] = "normal" return data - elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')): + elif key in (curses.KEY_ENTER, ord('\n'), ord('\r'), ord(":"), ord(";")): # Execute commands - execute(stdscr, height, width, commands) + data = execute(stdscr, data, commands) # Clear the bottom bar - stdscr.addstr(height - 1, 0, " " * (width - 1)) + stdscr.addstr(data["height"] - 1, 0, " " * (data["width"] - 1)) + # Return to normal mode after executing a command + data["mode"] = "normal" 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): + if chr(key) in valid and len(commands) < (data["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) + stdscr.addstr(data["height"]-1, 1, friendly_command) + # Move the cursor the end of the commands - stdscr.move(height-1, len(commands)+1) + stdscr.move(data["height"]-1, len(commands)+1) diff --git a/modes/insert.py b/modes/insert.py index 9641028..41e2458 100644 --- a/modes/insert.py +++ b/modes/insert.py @@ -1,17 +1,33 @@ -from core import statusbar +from core import statusbar, cursor -def execute(stdscr, height, width, key): - return +def execute(data, key): + return data -def activate(stdscr, height, width, data): +def activate(stdscr, data): # Refresh the status bar with a different colour for insert - colors = [8, 12, 14, 2] - statusbar.refresh(stdscr, height, width, "INSERT", colors) + data["statusbar_colors"] = [8, 12, 14, 2] + statusbar.refresh(stdscr, data) - while True: - # Wait for and capture a key press from the user - key = stdscr.getch() + # Refresh the status bar + statusbar.refresh(stdscr, data) + # Move the cursor + cursor.move(stdscr, data) + + # Switch to a line cursor + cursor.cursor_mode("line") + + # Wait for and capture a key press from the user + key = stdscr.getch() + + # Exit insert mode + if key == 27: + data["mode"] = "normal" return data + + # Check keybindings + data = execute(data, key) + + return data diff --git a/modes/normal.py b/modes/normal.py index cbd4893..cd90104 100644 --- a/modes/normal.py +++ b/modes/normal.py @@ -1,9 +1,7 @@ -from modes import command -from modes import insert -from core import statusbar +from core import statusbar, cursor -def execute(stdscr, height, width, key, data): +def execute(data, key): if key == ord("j"): # Move the cursor down data["cursor_y"] += 1 @@ -21,23 +19,30 @@ def execute(stdscr, height, width, key, data): data["cursor_x"] -= 1 elif key == ord("i"): - # Insert mode - data = insert.activate(stdscr, height, width, data) + # Exit normal mode and enter insert mode + data["mode"] = "insert" elif key in (ord(":"), ord(";")): - # Switch to command mode - data = command.activate(stdscr, height, width, data) + # Exit normal mode and enter command mode + data["mode"] = "command" return data -def activate(stdscr, height, width, data): +def activate(stdscr, data): # Refresh the status bar - statusbar.refresh(stdscr, height, width, "NORMAL") + statusbar.refresh(stdscr, data) + + # Move the cursor + cursor.move(stdscr, data) + + # Switch the cursor to a block + cursor.cursor_mode("block") # Wait for and capture a key press from the user key = stdscr.getch() # Check against the keybindings - data = execute(stdscr, height, width, key, data) + data = execute(data, key) + return data