first commit
This commit is contained in:
commit
ddf99d71f6
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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
|
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredIdentifiers">
|
||||||
|
<list>
|
||||||
|
<option value="normal" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
11
.idea/lambda.iml
Normal file
11
.idea/lambda.iml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="CPP_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/lambda.iml" filepath="$PROJECT_DIR$/.idea/lambda.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"./modes"
|
||||||
|
]
|
||||||
|
}
|
BIN
__pycache__/colour.cpython-310.pyc
Normal file
BIN
__pycache__/colour.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/colors.cpython-310.pyc
Normal file
BIN
core/__pycache__/colors.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/cursor.cpython-310.pyc
Normal file
BIN
core/__pycache__/cursor.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/statusbar.cpython-310.pyc
Normal file
BIN
core/__pycache__/statusbar.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/utils.cpython-310.pyc
Normal file
BIN
core/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
20
core/colors.py
Normal file
20
core/colors.py
Normal file
@ -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)
|
5
core/cursor.py
Normal file
5
core/cursor.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
def cursor_mode(mode):
|
||||||
|
if mode == "block":
|
||||||
|
print("\033[2 q")
|
||||||
|
elif mode == "line":
|
||||||
|
print("\033[6 q")
|
35
core/statusbar.py
Normal file
35
core/statusbar.py
Normal file
@ -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]))
|
28
core/utils.py
Normal file
28
core/utils.py
Normal file
@ -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))
|
85
lambda
Executable file
85
lambda
Executable file
@ -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 <file> to open a file and edit",
|
||||||
|
"Type :q or <C-c> 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()
|
BIN
modes/__pycache__/command.cpython-310.pyc
Normal file
BIN
modes/__pycache__/command.cpython-310.pyc
Normal file
Binary file not shown.
BIN
modes/__pycache__/command_mode.cpython-310.pyc
Normal file
BIN
modes/__pycache__/command_mode.cpython-310.pyc
Normal file
Binary file not shown.
BIN
modes/__pycache__/insert.cpython-310.pyc
Normal file
BIN
modes/__pycache__/insert.cpython-310.pyc
Normal file
Binary file not shown.
BIN
modes/__pycache__/normal.cpython-310.pyc
Normal file
BIN
modes/__pycache__/normal.cpython-310.pyc
Normal file
Binary file not shown.
71
modes/command.py
Normal file
71
modes/command.py
Normal file
@ -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)
|
17
modes/insert.py
Normal file
17
modes/insert.py
Normal file
@ -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
|
43
modes/normal.py
Normal file
43
modes/normal.py
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user