package ncure
import "core:strings"
import "core:strconv"
import "core:os"
import "core:fmt"
import "core:mem"
import "../linux"
ESC :: "\e";
SEQUENCE_START :: "\e[";
NEWLINE :: "\n";
CLEAR :: "\e[2J";
CLEAR_DOWN :: "\e[J";
CLEAR_UP :: "\e[1J";
CLEAR_LINE :: "\e[2K";
CLEAR_LINE_RIGHT :: "\e[K";
CLEAR_LINE_LEFT :: "\e[1K";
TOP_LEFT :: "\e[1;1H";
GET_CURSOR :: "\e[6n";
HIDE_CURSOR :: "\e[?25l";
SHOW_CURSOR :: "\e[?25h";
SAVE_CURSOR :: "\e7";
RESTORE_CURSOR :: "\e8";
MOVE_UP :: "\e[1A";
MOVE_DOWN :: "\e[1B";
MOVE_LEFT :: "\e[1D";
MOVE_RIGHT :: "\e[1C";
BatchInfo :: struct {
builder: strings.Builder,
cursor: CursorPos, // Current cursor position at latest ncure call. NOTE: Doesn't necessarily work properly atm.
cursor_start: CursorPos, // Cursor position at the start of a batch
savedCursor: bool,
savedCursorPos: CursorPos, // NOTE: Doesn't necessarily work atm.
termSize: TermSize,
}
@private
_batch := false;
@private
_batchInfo: ^BatchInfo = nil; // TODO: Switch to a stack thing so we can have nested Batches
@private
_createBatchInfo :: proc(batchInfo: ^BatchInfo) {
batchInfo.builder = strings.make_builder();
batchInfo.cursor = getCursor();
batchInfo.cursor_start = batchInfo.cursor;
batchInfo.termSize = getTermSize();
batchInfo.savedCursor = false;
_batchInfo = batchInfo;
}
@private
_destroyBatchInfo :: proc(batchInfo: ^BatchInfo) {
strings.destroy_builder(&batchInfo.builder);
}
batch_start :: proc() -> ^BatchInfo { // TODO
batchInfo: ^BatchInfo = cast(^BatchInfo) mem.alloc(size_of(BatchInfo));
_createBatchInfo(batchInfo);
_batch = true;
return batchInfo;
}
batch_end :: proc() {
if _batch != true do return;
os.write_string(os.stdout, strings.to_string(_batchInfo.builder));
_destroyBatchInfo(_batchInfo);
_batch = false;
}
// TODO: This isn't thread-safe at all
batch :: proc(p: #type proc(batchInfo: ^BatchInfo, args: ..any), args: ..any) {
// State for Batch Build: builder, cursor, and termsize
batchInfo: BatchInfo;
_createBatchInfo(&batchInfo);
defer _destroyBatchInfo(&batchInfo);
_batch = true;
p(&batchInfo, ..args);
os.write_string(os.stdout, strings.to_string(batchInfo.builder));
_batch = false;
}
getTermSize :: proc() -> (termSize: TermSize) {
w: linux.winsize;
if _, err := linux.ioctl(os.stdout, linux.TIOCGWINSZ, &w); err != os.ERROR_NONE {
// Error
}
termSize.width = int(w.ws_col);
termSize.height = int(w.ws_row);
return termSize;
}
getCursor :: proc() -> CursorPos {
if _batch do return _batchInfo.cursor;
cursor: CursorPos;
// Disable Echo, send request, then switch terminal
// back to previous settings
prev, _ := disableEcho(false);
os.write_string(os.stdout, GET_CURSOR);
if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, &prev); set_error != os.ERROR_NONE {
fmt.println("Error setting terminal info: %s\n", set_error);
}
// Get response
response := strings.make_builder();
defer strings.destroy_builder(&response);
data: byte;
for {
data = getch();
strings.write_byte(&response, data);
if data == 'R' do break;
}
// Parse response
response_str := strings.to_string(response);
arg1_start: int;
arg1_end: int;
arg2_start: int;
arg2_end: int;
for c, i in response_str {
if c == '[' do arg1_start = i + 1;
if c == ';' {
arg1_end = i;
arg2_start = i + 1;
}
if c == 'R' {
arg2_end = i;
}
}
arg1 := response_str[arg1_start:arg1_end];
arg2 := response_str[arg2_start:arg2_end];
cursor.y = strconv.atoi(arg1);
cursor.x = strconv.atoi(arg2);
return cursor;
}
getCursor_topleft :: proc() -> CursorPos {
return CursorPos {1, 1};
}
getCursor_topright :: proc(termSize: ^TermSize = nil) -> CursorPos {
new_ts: TermSize;
if _batch {
new_ts = _batchInfo.termSize;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
}
return CursorPos {new_ts.width, 1};
}
getCursor_bottomleft :: proc(termSize: ^TermSize = nil) -> CursorPos {
new_ts: TermSize;
if _batch {
new_ts = _batchInfo.termSize;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
}
return CursorPos {1, new_ts.height};
}
getCursor_bottomright :: proc(termSize: ^TermSize = nil) -> CursorPos {
new_ts: TermSize;
if _batch {
new_ts = _batchInfo.termSize;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
}
return CursorPos {new_ts.width, new_ts.height};
}
hideCursor :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, HIDE_CURSOR);
} else {
os.write_string(os.stdout, HIDE_CURSOR);
}
}
showCursor :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, SHOW_CURSOR);
} else {
os.write_string(os.stdout, SHOW_CURSOR);
}
}
saveCursor :: proc(overwrite := false) {
if !overwrite {
assert(!_batchInfo.savedCursor, "A cursor has already been saved without being restored.");
}
if _batch {
strings.write_string(&_batchInfo.builder, SAVE_CURSOR);
// Set savedCursor so that subsequent commands know when a saved cursor will be overridden
_batchInfo.savedCursor = true;
_batchInfo.savedCursorPos = _batchInfo.cursor;
} else {
os.write_string(os.stdout, SAVE_CURSOR);
}
}
restoreCursor :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, RESTORE_CURSOR);
// Set savedCursor so that subsequent commands know when a saved cursor is being overridden
_batchInfo.savedCursor = false;
_batchInfo.cursor = _batchInfo.savedCursorPos;
} else {
os.write_string(os.stdout, RESTORE_CURSOR);
}
}
// TODO: Add option to do something like this in the batching stuff??
save_restore :: proc(cursor: CursorPos, f: #type proc()) {
saveCursor();
setCursor(cursor);
f();
restoreCursor();
}
getSequence_set :: proc(x, y: int, b: ^strings.Builder = nil) -> string {
if x == 1 && y == 1 {
if b != nil {
strings.write_string(b, TOP_LEFT);
return strings.to_string(b^);
}
return strings.clone(TOP_LEFT);
}
buf: [129]byte;
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
if y == 1 do strings.write_string(builder, "1;");
else {
strings.write_string(builder, strconv.itoa(buf[:], y));
strings.write_rune(builder, ';');
}
if x == 1 do strings.write_string(builder, "1H");
else {
strings.write_string(builder, strconv.itoa(buf[:], x));
strings.write_rune(builder, 'H');
}
return strings.to_string(builder^);
}
getSequence_moveup :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_UP);
return strings.to_string(b^);
}
return strings.clone(MOVE_UP);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'A');
return strings.to_string(builder^);
}
getSequence_movedown :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_DOWN);
return strings.to_string(b^);
}
return strings.clone(MOVE_DOWN);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'B');
return strings.to_string(builder^);
}
getSequence_moveleft :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_LEFT);
return strings.to_string(b^);
}
return strings.clone(MOVE_LEFT);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'D');
return strings.to_string(builder^);
}
getSequence_moveright :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_RIGHT);
return strings.to_string(b^);
}
return strings.clone(MOVE_RIGHT);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'C');
return strings.to_string(builder^);
}
setCursor_xy :: proc(x, y: int, cursor: ^CursorPos = nil, savePrev := false) {
str: string;
defer delete(str);
if savePrev {
saveCursor();
}
if _batch {
str := getSequence_set(x, y, &_batchInfo.builder);
_batchInfo.cursor.x = x;
_batchInfo.cursor.y = y;
} else {
str := getSequence_set(x, y);
defer delete(str);
os.write_string(os.stdout, str);
}
if cursor != nil {
cursor.x = x;
cursor.y = y;
}
}
setCursor_cursor :: proc(cursor: CursorPos, savePrev := false) {
setCursor_xy(x = cursor.x, y = cursor.y, savePrev = savePrev);
}
setCursor :: proc{setCursor_xy, setCursor_cursor};
setCursor_topleft :: proc(cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
if _batch {
strings.write_string(&_batchInfo.builder, TOP_LEFT);
_batchInfo.cursor.x = 1;
_batchInfo.cursor.y = 1;
} else {
os.write_string(os.stdout, TOP_LEFT);
}
if cursor != nil {
cursor.x = 1;
cursor.y = 1;
}
}
setCursor_topright :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
c := getCursor_topright(termSize);
setCursor(c);
if cursor != nil do cursor^ = c;
}
setCursor_bottomleft :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
c := getCursor_bottomleft(termSize);
setCursor(c);
if cursor != nil do cursor^ = c;
}
setCursor_bottomright :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
c := getCursor_bottomright(termSize);
setCursor(c);
if cursor != nil do cursor^ = c;
}
// TODO: Add optional cursor argument to be set
moveCursor_up :: proc(amt: int = 1) {
if _batch {
str := getSequence_moveup(amt, &_batchInfo.builder);
_batchInfo.cursor.y -= amt;
} else {
str := getSequence_moveup(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_down :: proc(amt: int = 1) {
if _batch {
str := getSequence_movedown(amt, &_batchInfo.builder);
_batchInfo.cursor.y += amt;
} else {
str := getSequence_movedown(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_left :: proc(amt: int = 1) {
if _batch {
str := getSequence_moveleft(amt, &_batchInfo.builder);
_batchInfo.cursor.x -= amt;
} else {
str := getSequence_moveleft(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_right :: proc(amt: int = 1) {
if _batch {
str := getSequence_moveright(amt, &_batchInfo.builder);
_batchInfo.cursor.x += amt;
} else {
str := getSequence_moveright(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_start :: proc() {
if _batch {
strings.write_byte(&_batchInfo.builder, '\r');
_batchInfo.cursor.x = 1;
} else {
os.write_byte(os.stdout, '\r');
}
}
moveCursor_end :: proc(termSize: ^TermSize = nil) {
new_ts: TermSize;
moveCursor_start();
if _batch {
new_ts = _batchInfo.termSize;
getSequence_moveright(new_ts.width, &_batchInfo.builder);
_batchInfo.cursor.x = new_ts.width;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
str := getSequence_moveright(new_ts.width);
os.write_string(os.stdout, str);
}
}
// TODO: The write and print functions don't change the cursor position correctly
// due to needing to scan the string for escape sequences, new lines, \b,
// non-printable characters, and combinational utf-8 characters
write_string_nocolor :: proc(s: string) {
if _batch {
strings.write_string(&_batchInfo.builder, s);
_batchInfo.cursor.x += len(s); // TODO: This would not work with \b, non-printable chars, and escape sequences within the string
} else {
os.write_string(os.stdout, s);
}
}
write_string_at_nocolor :: proc(cursor: CursorPos, s: string) {
saveCursor();
setCursor(cursor);
write_string_nocolor(s);
restoreCursor();
}
write_string_color :: proc(fg: ForegroundColor, s: string) {
setColor(fg);
if _batch {
strings.write_string(&_batchInfo.builder, s);
_batchInfo.cursor.x += len(s); // TODO: This would not work with \b, non-printable chars, and escape sequences within the string
} else {
os.write_string(os.stdout, s);
}
resetColors();
}
write_string_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, s: string) {
saveCursor();
setCursor(cursor);
write_string_color(fg, s);
restoreCursor();
}
write_string :: proc{write_string_nocolor, write_string_color, write_string_at_nocolor, write_string_at_color};
// TODO: write_strings functions with ..string arg, but doesn't use print/printf/println
write_strings_nocolor :: proc(args: ..string) {
for s in args {
write_string(s);
}
}
write_strings_at_nocolor :: proc(cursor: CursorPos, args: ..string) {
saveCursor();
setCursor(cursor);
write_strings_nocolor(..args);
restoreCursor();
}
write_strings_color :: proc(fg: ForegroundColor, args: ..string) {
for s in args {
write_string(fg, s);
}
}
write_strings_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..string) {
saveCursor();
setCursor(cursor);
write_strings_color(fg, ..args);
restoreCursor();
}
write_strings :: proc{write_strings_nocolor, write_strings_color, write_strings_at_nocolor, write_strings_at_color};
write_line_nocolor :: proc(s: string) {
if _batch {
strings.write_string(&_batchInfo.builder, s);
} else {
os.write_string(os.stdout, s);
}
newLine();
}
write_line_at_nocolor :: proc(cursor: CursorPos, s: string) {
saveCursor();
setCursor(cursor);
write_line_nocolor(s);
restoreCursor();
}
write_line_color :: proc(fg: ForegroundColor, s: string) {
setColor(fg);
if _batch {
strings.write_string(&_batchInfo.builder, s);
} else {
os.write_string(os.stdout, s);
}
resetColors();
newLine();
}
write_line_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, s: string) {
saveCursor();
setCursor(cursor);
write_line_color(fg, s);
restoreCursor();
}
write_line :: proc{write_line_nocolor, write_line_color, write_line_at_nocolor, write_line_at_color};
write_byte_current :: proc(b: byte) {
if _batch {
strings.write_byte(&_batchInfo.builder, b);
_batchInfo.cursor.x += 1;
} else {
os.write_byte(os.stdout, b);
}
}
write_byte_at :: proc(cursor: CursorPos, b: byte) {
saveCursor();
setCursor(cursor);
write_byte_current(b);
restoreCursor();
}
write_byte :: proc{write_byte_current, write_byte_at};
write_rune_current :: proc(r: rune) {
if _batch {
strings.write_rune(&_batchInfo.builder, r);
_batchInfo.cursor.x += 1; // TODO: non-printable/combinational rune
} else {
os.write_rune(os.stdout, r);
}
}
write_rune_at :: proc(cursor: CursorPos, r: rune) {
saveCursor();
setCursor(cursor);
write_rune_current(r);
restoreCursor();
}
write_rune :: proc{write_rune_current, write_rune_at};
// TODO: Not sure how to handle separator
print_nocolor :: proc(args: ..any, sep := " ") {
if _batch {
fmt.sbprint(&_batchInfo.builder, ..args);
} else {
fmt.print(..args);
}
}
print_at_nocolor :: proc(cursor: CursorPos, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
print_nocolor(..args);
restoreCursor();
}
print_color :: proc(fg: ForegroundColor, args: ..any, sep := " ") {
setColor(fg);
if _batch {
fmt.sbprint(&_batchInfo.builder, ..args);
} else {
fmt.print(..args);
}
resetColors();
}
print_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
print_color(fg, ..args);
restoreCursor();
}
print :: proc{print_nocolor, print_color, print_at_nocolor, print_at_color};
println_nocolor :: proc(args: ..any, sep := " ") {
if _batch {
fmt.sbprintln(&_batchInfo.builder, ..args);
_batchInfo.cursor.y += 1; // For the last newline
} else {
fmt.println(..args);
}
}
println_at_nocolor :: proc(cursor: CursorPos, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
println_nocolor(..args);
restoreCursor();
}
println_color :: proc(fg: ForegroundColor, args: ..any, sep := " ") {
setColor(fg);
if _batch {
fmt.sbprintln(&_batchInfo.builder, ..args);
_batchInfo.cursor.y += 1; // For the last newline
} else {
fmt.println(..args);
}
resetColors();
}
println_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
println_color(fg, ..args);
restoreCursor();
}
println :: proc{println_nocolor, println_color, println_at_nocolor, println_at_color};
printf_nocolor :: proc(format: string, args: ..any) {
if _batch {
fmt.sbprintf(&_batchInfo.builder, format, ..args);
} else {
fmt.printf(format, ..args);
}
}
printf_at_nocolor :: proc(cursor: CursorPos, format: string, args: ..any) {
saveCursor();
setCursor(cursor);
printf_nocolor(format, ..args);
restoreCursor();
}
printf_color :: proc(fg: ForegroundColor, format: string, args: ..any) {
setColor(fg);
if _batch {
fmt.sbprintf(&_batchInfo.builder, format, ..args);
} else {
fmt.printf(format, ..args);
}
resetColors();
}
printf_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, format: string, args: ..any) {
saveCursor();
setCursor(cursor);
printf_color(fg, format, ..args);
restoreCursor();
}
printf :: proc{printf_nocolor, printf_color, printf_at_nocolor, printf_at_color};
newLine :: proc(amt: int = 1) {
if _batch {
for i in 0..<amt {
strings.write_string(&_batchInfo.builder, NEWLINE);
}
_batchInfo.cursor.x = 1;
_batchInfo.cursor.y += amt;
} else {
for i in 0..<amt {
os.write_string(os.stdout, NEWLINE);
}
}
}
clearScreen :: proc() {
if _batch {
// Clearing the screen with erase everything before it.
// Therefore, we can reset everything that was already in
// the string builder
strings.reset_builder(&_batchInfo.builder);
strings.write_string(&_batchInfo.builder, CLEAR);
} else {
os.write_string(os.stdout, CLEAR);
}
}
clearLine :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, CLEAR_LINE);
} else {
os.write_string(os.stdout, CLEAR_LINE);
}
}
clearLine_right :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, CLEAR_LINE_RIGHT);
} else {
os.write_string(os.stdout, CLEAR_LINE_RIGHT);
}
}
clearLine_left :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, CLEAR_LINE_LEFT);
} else {
os.write_string(os.stdout, CLEAR_LINE_LEFT);
}
}
backspace :: proc(amt := 1, clear := true) {
if _batch {
// TODO: This doesn't handle escape sequences, non-printable characters, or combinational characters
// TODO: Problem - doing a backspace after a backspace that has added escape sequences will result
// in the deletion of some of the previous backspace, potentially.
/*for i in 0..<min(amt, strings.builder_len(_batchInfo.builder)) {
strings.pop_rune(&_batchInfo.builder);
}*/
// If trying to backspace more than what was buffered, then
// just add new escape sequences to the buffer to do this.
// diff := amt - strings.builder_len(_batchInfo.builder);
diff := amt;
if (diff > 0) {
moveCursor_left(diff);
if clear do clearLine_right();
else {
for i in 0..<diff {
os.write_string(os.stdout, " ");
}
moveCursor_left(diff);
}
}
} else {
moveCursor_left(amt);
if clear do clearLine_right();
else {
for i in 0..<amt {
os.write_string(os.stdout, " ");
}
moveCursor_left(amt);
}
}
}