# 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.. 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_cols: line = line[0.. screen_cols: bar = bar[0..= 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..= 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")