import curses
import sys
import os
from dataclasses import dataclass
# Shortcut Dictionary
shortcutKeys={}
def debug_buffer_to_file(buffer, filepath="debug_log.txt"):
with open(filepath, "w", encoding="utf-8") as f:
for i, line in enumerate(buffer):
text_line = "".join(chr(c) for c in line)
ascii_line = " ".join(str(c) for c in line)
f.write(f"{i:03d}: {text_line}\n")
f.write(f" ASCII: {ascii_line}\n")
def window_init():
pass
def load_file(buffer, filepath):
"""
This function opens a file provided by the user,
converts the lines from standard strings to a series of characetrs which are easier,
for our buffer array to keep track of and modify.
After converting the lines they're loaded into the buffer for modification.
(this uses a nested for that loops through the lines array creating an ascii line array to store the asci data
It then loops through the individual lines and converts them to their ascii numbers
After which the ascii line array is stored in the buffer)
"""
#[1}basic error catching system which "tries" to open a provided file
try:
with open(filepath, "r", encoding="utf-8")as file:
lines = file.read().splitlines()
for line in lines:
ascii_line = []
for char in line:
ascii_line.append(ord(char))
buffer.append(ascii_line)
#{2] if it cannot open the file it will print an error message and then create a blank line instead of crashing the program.
except (FileNotFoundError, PermissionError) as e:
print(f"[!] Could not open file '{filepath}': {e}")
buffer.append([])
return buffer
def save_file(buffer, filepath):
"""This function opens the file provided by the user,
Converts the lines found in the buffer back to standard strings,
after which it writes these lines to the file and closes it"""
content = ""
for line in buffer:
for column in line:
content += chr(column)
content += "\n"
with open(filepath, "w", encoding="utf-8") as file:
file.write(content)
def buffer_renderer(screen, buffer: list, row: int, column: int, r_offset: int, c_offset: int):
"""This function handles rendering whats in the buffer to the terminal screen.
It loops through each row and adds a buffer row to represent it.
If the buffer rows extend past the length of the buffer,
it moves the screen down by a row and clears the screen.
For each character column, offset by the horizontal scroll (c_offset),
it tries to render the character from the buffer.
If the buffer column is out of bounds for a given row (e.g., the line is shorter than the visible width),
it skips rendering at that position, leaving the rest of the screen cell empty or cleared."""
for rw in range(row):
buffer_row = rw + r_offset
if buffer_row >=len(buffer):
screen.move(rw, 0)
screen.clrtoeol()
continue
for cl in range (column):
buffer_column = cl + c_offset
screen.move(rw, cl)
try:
screen.addch(rw, cl,buffer[buffer_row][buffer_column])
except IndexError:
break
except curses.error:
pass
screen.clrtoeol()
pass
def visual_cursor_renderer(screen, row, row_offset, column, column_offset):
"""This function handles rendering the visual cursor
The visual cursor is the cursor found inside the terminal"""
try:
curses.curs_set(0)
except curses.error:
pass
# Tell curses where to draw the visual cursor, based on scroll offset and logical cursor
screen.move(row - row_offset, column - column_offset)
try:
curses.curs_set(1)
except curses.error:
pass
#NOTE: This refresh might be unnecessary, will remove later
#screen.refresh()
class StatusWindow:
"""This class will house a simple window which displays various important pieces of information about a specific file
NOTE(
I may want to rename this and make it also responsible for any commands I want to implement in the future
)"""
def __init__(self,width):
pass
class ContentBufferWindow:
"""This class will create a curses.pad will be responsible for housing and displaying the workable area of the tezxt editor.
NOTE(
Ideally i want to be able to create multiple pad instances in the same manager
)"""
def __init__(self, buffer, cursor, width, height):
self.buffer = buffer
self.cursor = cursor
self.width = width
self.height = height
self.pad_height = max(len(self.buffer) + 50, self.height)
self.pad = curses.newpad(self.pad_height, width)
def content_renderer(self):
self.pad.clear()
for rw in range(self.pad_height):
buffer_row = rw + self.cursor.row
if buffer_row >=len(self.buffer):
self.pad.move(rw, 0)
self.pad.clrtoeol()
continue
#break
for cl in range (self.width):
buffer_column = cl + self.cursor.column
self.pad.move(rw, cl)
try:
self.pad.addch(rw, cl,self.buffer[buffer_row][buffer_column])
except IndexError:
break
except curses.error:
pass
self.pad.clrtoeol()
try:
self.pad.refresh(
self.cursor.row_offset,
self.cursor.column_offset,
0, 0,
self.height - 1,
self.width - 1
)
except curses.error:
pass
pass
class WorkspaceManager:
"""This class will manage:
>>The different windows created,
>>inputs passed
>>Commands maybe?(not sure yet)
>>etc(ill update as i go along)"""
def __init__(self,):
pass
@dataclass(slots=True)
class Cursor:
"""This class handles Cursor initialization and application.
It needs to be aware of the cursors logical position, meaning its position within the editors buffer
COMPONENTS:
>> buffer : A reference to the text buffer (a list of strings), allowing the cursor to interact with document content.
>> row : The current logical row index within the buffer.
>> column : The current logical column index within the current buffer row.
>> row_offset : The number of rows the screen has scrolled down from the top of the buffer; determines vertical viewport start.
>> column_offset : The number of columns the screen has scrolled right from the start of each line; determines horizontal viewport start.
"""
buffer: list
row_offset: int = 0
row: int = 0
column_offset: int = 0
column: int = 0
def cursor_scrolling(self, term_column, visible_rows):
"""This function takes the available column and row space and moves the visual and logical terminal between its boundries"""
if self.column < self.column_offset:
self.column_offset =self.column
if self.column >= self.column_offset + term_column:
self.column_offset = self.column - term_column +1
if self.row < self.row_offset:
self.row_offset = self.row
if self.row >= self.row_offset + visible_rows:
self.row_offset = self.row - visible_rows +1
def cursor_actions(self, ch):
"""This Function takes the character recieved from getch()
If the character is an arrow key it moves both the visual and logical cursor to the new position"""
#LeftMovement
if ch == curses.KEY_LEFT:
if self.column !=0:
self.column -=1
elif self.row > 0:
self.row -=1
self.column = len(self.buffer[self.row])
if ch == curses.KEY_RIGHT:
if self.column < len(self.buffer[self.row]):
self.column+=1
elif self.row<len(self.buffer)-1:
self.row +=1
self.column = 0
if ch == curses.KEY_UP:
if self.row >0:
self.row -=1
#self.column = len(self.buffer[self.row])
if ch ==curses.KEY_DOWN:
if self.row < len(self.buffer)-1:
self.row +=1
#self.column = len(self.buffer[self.row])
def main(stdscr):
# Initialize the screen
#NOTE(
# WILL MAKE A SPECIFIC FUNCTION FOR SCREEN INITIALIZATION
#)
mainscreen = stdscr
curses.noecho() # Makes it so that non letter inputs arent displayed on the screen when typing
curses.raw() # Make it so that characters are recieved as the are sent to the terminal
mainscreen.keypad(1) # Enables the special keys like arrows and whatnot
buffer = []
src ='noname.txt' # Sets the default file name
(term_row,term_column)=mainscreen.getmaxyx() # Sets the terminal dimensions
visible_rows = term_row-1 # Sets aside a row for the line indicator/bottom info giver thingy
cursor = Cursor(buffer=buffer) # Creates and store the cursor in its own class
content_window = ContentBufferWindow(buffer=buffer, cursor= cursor, width= term_column, height=term_row)
# Handles file loading
if len(sys.argv) ==2:
src = sys.argv[1]
load_file(buffer, src)
else:
load_file(buffer, src)
#NOTE(
# Not sure i need this waiting to see if this breaks an edge case or something
# The video i followed didnt go into great detail on why choices were made so i just keep commenting things out to text their usefullness
# Hence large blocks of commented out code may be found all about the program thee will be removed eventually.
#)
while True:
mainscreen.move(0,0) #positions the cursor to top left
#SCROLLING
cursor.cursor_scrolling(term_column=term_column, visible_rows= visible_rows)
#SCREEN RENDERING
#buffer_renderer(screen= mainscreen, buffer=buffer, row=visible_rows, column=term_column, r_offset=cursor.row_offset, c_offset= cursor.column_offset )
content_window.content_renderer()
#debug_buffer_to_file(buffer=buffer)
#STATUS BAR NOTE: Soon to be status window.
status = f"{cursor.row+1}/{len(buffer)} Col:{cursor.column+1} {int((cursor.row+1)/len(buffer)*100)}% {src}"
mainscreen.addstr(term_row - 1, 0, status[:term_column-1], curses.A_REVERSE)
#CURSOR RENDERING
visual_cursor_renderer(screen=mainscreen, row= cursor.row, column= cursor.column, row_offset= cursor.row_offset, column_offset= cursor.column_offset)
#screen.refresh()
#input manager
ch =mainscreen.getch()
#Text insert
if ch != ((ch) & 0x1f) and ch < 128:
buffer[cursor.row].insert(cursor.column, ch)
cursor.column +=1
#enter handling
if chr(ch) in "\n\r":
line = buffer[cursor.row][cursor.column:]
buffer[cursor.row] = buffer[cursor.row][:cursor.column]
cursor.row+=1
cursor.column = 0
buffer.insert(cursor.row, line)
#Backspace handling
if ch in [8,263]:
if cursor.column:
cursor.column -=1
del buffer[cursor.row][cursor.column]
elif cursor.row>0:
line = buffer[cursor.row][cursor.column:]
del buffer[cursor.row]
cursor.row -=1
cursor.column = len(buffer[cursor.row])
buffer[cursor.row] += line
#CURSOR ACTIONS
cursor.cursor_actions(ch=ch)
#NOTE: NO CLUE WHAT THIS DID, MIGHT HAVE BEEN SOME LEFT OVER CODE DEALING WITH BUFFER RENDERING
#if cursor.row < len(buffer):
#rw = buffer[cursor.row]
#else:
#None
#rwlen = len(rw) if rw is not None else 0
#if cursor.column >rwlen:
#cursor.column = rwlen
#save and quit
if ch == (ord("s") & 0x1f): # Ctrl+S
save_file(buffer=buffer, filepath=src)
if ch == (ord("q") & 0x1f): # Ctrl+Q
sys.exit()
curses.wrapper(main) #this protects the terminal