nipbox/editor.nim

278 lines
6.9 KiB
Nim

# SPDX-License-Identifier: LUL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus SDK.
# See legal/LICENSE_UNBOUND.md for license terms.
# MARKUS MAIWALD (ARCHITECT) | VOXIS FORGE (AI)
# Scribe v3: The Sovereign TUI Editor
# Phase 24: Full TUI with Navigation & Multi-Sector IO
import strutils, sequtils
import ../../libs/membrane/libc as lb
# --- CONSTANTS ---
const
KEY_CTRL_Q = char(17)
KEY_CTRL_S = char(19)
KEY_CTRL_X = char(24) # Alternative exit
KEY_ESC = char(27)
KEY_BACKSPACE = char(127)
KEY_ENTER = char(13) # CR
KEY_LF = char(10)
# --- STATE ---
var lines: seq[string]
var cursor_x: int = 0
var cursor_y: int = 0 # Line index in buffer
var scroll_y: int = 0 # Index of top visible line
var screen_rows: int = 20 # Fixed for now, or detect?
var screen_cols: int = 80
var filename: string = ""
var status_msg: string = "CTRL-S: Save | CTRL-Q: Quit"
var is_running: bool = true
# --- TERMINAL HELPERS ---
proc write_raw(s: string) =
if s.len > 0:
discard lb.write(cint(1), cast[pointer](unsafeAddr s[0]), csize_t(s.len))
proc term_clear() =
write_raw("\x1b[2J") # Clear entire screen
proc term_move(row, col: int) =
# ANSI is 1-indexed
write_raw("\x1b[" & $(row + 1) & ";" & $(col + 1) & "H")
proc term_hide_cursor() = write_raw("\x1b[?25l")
proc term_show_cursor() = write_raw("\x1b[?25h")
# --- FILE IO ---
proc load_file(fname: string) =
lines = @[]
let fd = lb.open(fname.cstring, 0)
if fd >= 0:
var content = ""
var buf: array[512, char]
while true:
let n = lb.read(fd, addr buf[0], 512)
if n <= 0: break
for i in 0..<n: content.add(buf[i])
discard lb.close(fd)
if content.len > 0:
lines = content.splitLines()
else:
lines.add("")
else:
# New File
lines.add("")
if lines.len == 0: lines.add("")
proc save_file() =
var content = lines.join("\n")
# Ensure trailing newline often expected
if content.len > 0 and content[^1] != '\n': content.add('\n')
# FLAGS: O_WRONLY(1) | O_CREAT(64) | O_TRUNC(512) = 577
let fd = lb.open(filename.cstring, 577)
if fd < 0:
status_msg = "Error: Save Failed (VFS Open)."
return
let n = lb.write(fd, cast[pointer](unsafeAddr content[0]), csize_t(content.len))
discard lb.close(fd)
status_msg = "Saved " & $n & " bytes."
# --- LOGIC ---
proc scroll_to_cursor() =
if cursor_y < scroll_y:
scroll_y = cursor_y
if cursor_y >= scroll_y + screen_rows:
scroll_y = cursor_y - screen_rows + 1
proc render() =
term_hide_cursor()
term_move(0, 0)
# Draw Content
for i in 0..<screen_rows:
let line_idx = scroll_y + i
term_move(i, 0)
write_raw("\x1b[K") # Clear Line
if line_idx < lines.len:
var line = lines[line_idx]
# Truncate for display if needed? For now wrap or let terminal handle
if line.len > screen_cols: line = line[0..<screen_cols]
write_raw(line)
else:
write_raw("~") # Vim style empty lines
# Draw Status Bar
term_move(screen_rows, 0)
write_raw("\x1b[7m") # Invert
var bar = " " & filename & " - " & $cursor_x & ":" & $cursor_y & " | " & status_msg
while bar.len < screen_cols: bar.add(" ")
if bar.len > screen_cols: bar = bar[0..<screen_cols]
write_raw(bar)
write_raw("\x1b[0m") # Reset
# Position Cursor
term_move(cursor_y - scroll_y, cursor_x)
term_show_cursor()
proc insert_char(c: char) =
if cursor_y >= lines.len: lines.add("")
var line = lines[cursor_y]
if cursor_x > line.len: cursor_x = line.len
if cursor_x == line.len:
line.add(c)
else:
line.insert($c, cursor_x)
lines[cursor_y] = line
cursor_x += 1
proc insert_newline() =
if cursor_y >= lines.len: lines.add("") # Should catch
let current_line = lines[cursor_y]
if cursor_x >= current_line.len:
# Append new empty line
lines.insert("", cursor_y + 1)
else:
# Split line
let left = current_line[0..<cursor_x]
let right = current_line[cursor_x..^1]
lines[cursor_y] = left
lines.insert(right, cursor_y + 1)
cursor_y += 1
cursor_x = 0
proc backspace() =
if cursor_y >= lines.len: return
if cursor_x > 0:
var line = lines[cursor_y]
# Delete char at x-1
if cursor_x - 1 < line.len:
line.delete(cursor_x - 1, cursor_x - 1)
lines[cursor_y] = line
cursor_x -= 1
elif cursor_y > 0:
# Merge with previous line
let current = lines[cursor_y]
let prev_len = lines[cursor_y - 1].len
lines[cursor_y - 1].add(current)
lines.delete(cursor_y)
cursor_y -= 1
cursor_x = prev_len
proc handle_arrow(code: char) =
case code:
of 'A': # UP
if cursor_y > 0: cursor_y -= 1
of 'B': # DOWN
if cursor_y < lines.len - 1: cursor_y += 1
of 'C': # RIGHT
if cursor_y < lines.len:
if cursor_x < lines[cursor_y].len: cursor_x += 1
elif cursor_y < lines.len - 1: # Wrap to next line
cursor_y += 1
cursor_x = 0
of 'D': # LEFT
if cursor_x > 0: cursor_x -= 1
elif cursor_y > 0: # Wrap to prev line end
cursor_y -= 1
cursor_x = lines[cursor_y].len
else: discard
# Snap cursor to line length
if cursor_y < lines.len:
if cursor_x > lines[cursor_y].len: cursor_x = lines[cursor_y].len
# --- MAIN LOOP ---
proc read_input() =
# We need a custom input loop that handles escapes
# This uses libc.read on fd 0 (stdin)
var c: char
let n = lb.read(0, addr c, 1)
if n <= 0: return
if c == KEY_CTRL_Q or c == KEY_CTRL_X:
is_running = false
return
if c == KEY_CTRL_S:
save_file()
return
if c == KEY_ESC:
# Potential Sequence
# Busy wait briefly for next char to confirm sequence vs lone ESC
# In a real OS we'd have poll/timeout. Here we hack.
# Actually, let's just try to read immediately.
var c2: char
let n2 = lb.read(0, addr c2, 1) # This might block if not buffered?
# Our lb.read is non-blocking if ring is empty, returns 0.
# But for a sequence, the chars should be in the packet together or close.
# If 0, it was just ESC.
if n2 > 0 and c2 == '[':
var c3: char
let n3 = lb.read(0, addr c3, 1)
if n3 > 0:
handle_arrow(c3)
return
if c == KEY_BACKSPACE or c == '\b':
backspace()
return
if c == KEY_ENTER or c == KEY_LF:
insert_newline()
return
# Normal char
if c >= ' ' and c <= '~':
insert_char(c)
proc start_editor*(fname: string) =
filename = fname
is_running = true
cursor_x = 0
cursor_y = 0
scroll_y = 0
status_msg = "CTRL-S: Save | CTRL-Q: Quit"
write_raw("[Scribe] Loading " & fname & "...\n")
load_file(fname)
term_clear()
while is_running:
# lb.pump_membrane_stack() - Handled by Kernel
scroll_to_cursor()
render()
# Input Loop (Non-blocking check mostly)
# We loop quickly to feel responsive
read_input()
# Yield slightly
for i in 0..5000: discard
term_clear()
term_move(0, 0)
write_raw("Scribe Closed.\n")