2 Commits

Author SHA1 Message Date
881e2b3393 Continue C++ rewrite. 2025-11-25 09:17:50 -08:00
bbb23916a0 Start C++ rewrite. 2025-11-24 23:36:19 -08:00
37 changed files with 4375 additions and 4446 deletions

View File

@@ -1,37 +1,74 @@
# CMake configuration for Kyle's Editor (ke)
cmake_minimum_required(VERSION 3.15)
project(ke C) # Specify C language explicitly
project(ke VERSION 1.3.3 LANGUAGES CXX)
set(CMAKE_C_STANDARD 99)
set(KE_VERSION "2.1.4")
# Project metadata
set(KE_VERSION "1.3.3")
set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEFAULT_SOURCE -D_XOPEN_SOURCE")
# C++17 standard requirement
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Optionally enable AddressSanitizer (ASan)
# Source files
set(KE_SOURCES
main.cc
abuf.cc
erow.cc
terminal.cc
input_handler.cc
display.cc
file_io.cc
killring.cc
)
# Header files
set(KE_HEADERS
ke_constants.h
abuf.h
erow.h
terminal.h
input_handler.h
display.h
file_io.h
killring.h
)
# Build options
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
if (ENABLE_ASAN)
# Create executable target
add_executable(ke ${KE_SOURCES})
# Target compile features
target_compile_features(ke PRIVATE cxx_std_17)
# Target compile options
target_compile_options(ke PRIVATE
-Wall
-Wextra
-pedantic
-Wshadow
-Werror
-g
)
# Target compile definitions
target_compile_definitions(ke PRIVATE
KE_VERSION="ke version ${KE_VERSION}"
_DEFAULT_SOURCE
_XOPEN_SOURCE
)
# AddressSanitizer support
if(ENABLE_ASAN)
message(STATUS "ASan enabled")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
# Ensure the sanitizer is linked too (especially important on some platforms)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
target_compile_options(ke PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(ke PRIVATE -fsanitize=address)
endif()
# Installation rules
include(GNUInstallDirs)
# Add executable
add_executable(ke
abuf.c
term.c
buffer.c
editor.c
core.c
core.h
main.c
)
target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}")
install(TARGETS ke RUNTIME DESTINATION bin)
install(FILES ke.1 TYPE MAN)
install(TARGETS ke RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ke.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)

View File

@@ -2,20 +2,53 @@ TARGET := ke
KE_VERSION := devel
DEST := $(HOME)/.local/bin/$(TARGET)
CC := gcc
CXX := g++
CFLAGS := -Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g
CFLAGS += -Wno-unused-result
CFLAGS += -D_DEFAULT_SOURCE -D_XOPEN_SOURCE
CFLAGS += -fsanitize=address -fno-omit-frame-pointer
CXXFLAGS := -Wall -Wextra -pedantic -Wshadow -Werror -std=c++17 -g
CXXFLAGS += -Wno-unused-result
CXXFLAGS += -D_DEFAULT_SOURCE -D_XOPEN_SOURCE
CXXFLAGS += -DKE_VERSION='"ke $(KE_VERSION)"'
CXXFLAGS += -fsanitize=address -fno-omit-frame-pointer
LDFLAGS := -fsanitize=address
SOURCES := main.cc abuf.cc erow.cc terminal.cc input_handler.cc display.cc file_io.cc killring.cc
OBJECTS := main.o abuf.o erow.o terminal.o input_handler.o display.o file_io.o killring.o
all: $(TARGET) test.txt
SRCS := main.c abuf.c term.c buffer.c editor.c core.c
HDRS := abuf.h term.h buffer.h editor.h core.h
$(TARGET): $(OBJECTS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJECTS) $(LDFLAGS)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS)
main.o: main.cc ke_constants.h
$(CXX) $(CXXFLAGS) -c main.cc
abuf.o: abuf.cc abuf.h
$(CXX) $(CXXFLAGS) -c abuf.cc
erow.o: erow.cc erow.h
$(CXX) $(CXXFLAGS) -c erow.cc
terminal.o: terminal.cc terminal.h
$(CXX) $(CXXFLAGS) -c terminal.cc
input_handler.o: input_handler.cc input_handler.h ke_constants.h
$(CXX) $(CXXFLAGS) -c input_handler.cc
display.o: display.cc display.h ke_constants.h
$(CXX) $(CXXFLAGS) -c display.cc
file_io.o: file_io.cc file_io.h
$(CXX) $(CXXFLAGS) -c file_io.cc
killring.o: killring.cc killring.h
$(CXX) $(CXXFLAGS) -c killring.cc
.PHONY: install
#install: $(TARGET)
@@ -24,17 +57,9 @@ install:
clean:
rm -f $(TARGET)
rm -f $(OBJECTS)
rm -f asan.log*
.PHONY: test.txt
test.txt:
cp test.txt.bak $@
.PHONY: gdb
gdb:
@test -f $(TARGET).pid || (echo "error: $(TARGET).pid not found" >&2; exit 1)
@gdb -p $$(cat $(TARGET).pid | tr -d ' \t\n\r') ./$(TARGET)
.PHONY: cloc
cloc:
cloc $(SRCS) $(HDRS)

View File

@@ -16,9 +16,3 @@ To get verbose ASAN messages:
export LSAN_OPTIONS=verbosity=1:log_threads=1
Released under an ISC license.
Started by following along with kilo:
https://viewsourcecode.org/snaptoken/kilo/
E.g., in the devlogs
https://log.wntrmute.dev/2020/02/20200207

17
TODO
View File

@@ -1,18 +1,15 @@
[X] goto-line
[X] text-corruption bug
[ ] alt-modifiers
[x] refresh-screen
[ ] functions -> keymapping? (what did this even mean)
[ ] refresh-screen
[ ] functions -> keymapping?
[X] rendering: need to skip over control characters like we do with tabs
[X] control-d -> delete
[x] control-g -> exit buf prompt
[x] load-file prompt on dirty buffer
[ ] control-g -> exit buf prompt
[ ] load-file prompt on dirty buffer
[ ] multiple files
[x] utf-8
[x] kill ring
[x] mark/region
What would it take for ke to be your daily drives?
[x] C-u (repeat actions)
[x] Alt nav (backspace, delete, f, b, etc)
[-] undo tree (C-k u/U)
+ C-u
+ Alt nav (backspace, delete, f, b, etc)

107
abuf.c
View File

@@ -1,107 +0,0 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "abuf.h"
#include "core.h"
static void
abuf_grow(abuf *buf, size_t delta)
{
if (buf->cap - buf->size < delta) {
ab_resize(buf, buf->cap + delta);
}
}
void
ab_init(abuf *buf)
{
assert(buf != NULL);
buf->b = NULL;
buf->size = buf->cap = 0;
}
void
ab_init_cap(abuf *buf, const size_t cap)
{
ab_init(buf);
if (cap > 0) {
ab_resize(buf, cap);
}
}
void
ab_resize(abuf *buf, size_t cap)
{
char *newbuf = NULL;
cap = cap_growth(buf->cap, cap) + 1;
newbuf = realloc(buf->b, cap);
assert(newbuf != NULL);
buf->cap = cap;
buf->b = newbuf;
}
void
ab_appendch(abuf *buf, char c)
{
abuf_grow(buf, 1);
ab_append(buf, &c, 1);
}
void
ab_append(abuf *buf, const char *s, size_t len)
{
char *nc = NULL;
abuf_grow(buf, len);
nc = buf->b;
memcpy(&nc[buf->size], s, len);
buf->b = nc;
buf->size += len;
}
void
ab_prependch(abuf *buf, const char c)
{
abuf_grow(buf, 1);
ab_prepend(buf, &c, 1);
}
void
ab_prepend(abuf *buf, const char *s, const size_t len)
{
char *nc = NULL;
abuf_grow(buf, len);
nc = buf->b;
assert(nc != NULL);
memmove(nc + len, nc, buf->size);
memcpy(nc, s, len);
buf->b = nc;
buf->size += len;
}
void
ab_free(abuf *buf)
{
free(buf->b);
buf->b = NULL;
buf->size = 0;
buf->cap = 0;
}

17
abuf.cc Normal file
View File

@@ -0,0 +1,17 @@
#include "abuf.h"
namespace ke {
void abuf::append(std::string_view s) {
buffer_.append(s);
}
void abuf::append(const char* s, std::size_t len) {
buffer_.append(s, len);
}
void abuf::append(char c) {
buffer_.push_back(c);
}
} // namespace ke

79
abuf.h
View File

@@ -1,30 +1,57 @@
/*
* abuf.h - append/prepend buffer utilities
#ifndef ABUF_HPP
#define ABUF_HPP
#include <string>
#include <string_view>
#include <cstddef>
namespace ke {
/**
* Append buffer class for efficient string building.
* C++17 implementation with RAII memory management.
*/
#ifndef KE_ABUF_H
#define KE_ABUF_H
class abuf {
public:
// Constructors
abuf() noexcept = default;
// Deleted copy constructor and assignment (use move semantics)
abuf(const abuf&) = delete;
abuf& operator=(const abuf&) = delete;
// Move constructor and assignment
abuf(abuf&&) noexcept = default;
abuf& operator=(abuf&&) noexcept = default;
// Destructor (automatic cleanup via std::string)
~abuf() = default;
// Append methods
void append(std::string_view s);
void append(const char* s, std::size_t len);
void append(char c);
// Accessors
[[nodiscard]] const char* data() const noexcept { return buffer_.data(); }
[[nodiscard]] std::size_t size() const noexcept { return buffer_.size(); }
[[nodiscard]] std::size_t length() const noexcept { return buffer_.length(); }
[[nodiscard]] bool empty() const noexcept { return buffer_.empty(); }
[[nodiscard]] std::size_t capacity() const noexcept { return buffer_.capacity(); }
// Get the underlying string
[[nodiscard]] const std::string& str() const noexcept { return buffer_; }
// Clear the buffer
void clear() noexcept { buffer_.clear(); }
// Reserve capacity
void reserve(std::size_t new_cap) { buffer_.reserve(new_cap); }
#include <stddef.h>
private:
std::string buffer_;
};
} // namespace ke
typedef struct abuf {
char *b;
size_t size;
size_t cap;
} abuf;
#define ABUF_INIT {NULL, 0, 0}
void ab_init(abuf *buf);
void ab_init_cap(abuf *buf, size_t cap);
void ab_resize(abuf *buf, size_t cap);
void ab_appendch(abuf *buf, char c);
void ab_append(abuf *buf, const char *s, size_t len);
void ab_prependch(abuf *buf, const char c);
void ab_prepend(abuf *buf, const char *s, const size_t len);
void ab_free(abuf *buf);
#endif
#endif // ABUF_HPP

466
buffer.c
View File

@@ -1,466 +0,0 @@
/* buffer.c - multiple file buffers */
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "abuf.h"
#include "buffer.h"
#include "core.h"
#include "editor.h"
#define NO_NAME "[No Name]"
/* externs from other modules */
char *editor_prompt(const char *, void (*cb)(char *, int16_t));
static const char *
buf_basename(const char *path)
{
if (path == NULL) {
return NULL;
}
const char *slash = strrchr(path, '/');
return (slash != NULL) ? (slash + 1) : path;
}
static int
buffer_find_exact_by_name(const char *name)
{
buffer *b = NULL;
size_t i = 0;
if (name == NULL) {
return -1;
}
for (i = 0; i < editor.bufcount; i++) {
b = editor.buffers[i];
const char *full = b->filename;
const char *base = buf_basename(full);
if (full && strcmp(full, name) == 0) {
return i;
}
if (base && strcmp(base, name) == 0) {
return i;
}
if (full == NULL && strcmp(name, NO_NAME) == 0) {
return i;
}
}
return -1;
}
static int
buffer_collect_prefix_matches(const char *prefix, int *out_idx, const int max_out)
{
buffer *b = NULL;
int count = 0;
int matched = 0;
size_t i = 0;
size_t plen = (prefix ? strlen(prefix) : 0);
for (i = 0; i < editor.bufcount; i++) {
matched = 0;
b = editor.buffers[i];
const char *cand1 = b->filename;
const char *cand2 = buf_basename(cand1);
if (plen == 0) {
matched = 1; /* everything matches empty prefix */
} else {
if (cand2 && strncmp(cand2, prefix, plen) == 0) {
matched = 1;
} else if (cand1 && strncmp(cand1, prefix, plen) == 0) {
matched = 1;
} else if (!cand1 && strncmp(NO_NAME, prefix, plen) == 0) {
matched = 1;
}
}
if (matched) {
if (count < max_out) {
out_idx[count] = i;
}
count++;
}
}
return count;
}
static void
longest_common_prefix(char *buf, const size_t bufsz, const int *idxs, const int n)
{
const char *first = NULL;
const char *cand = NULL;
int k = 0;
size_t j = 0;
size_t cur = 0;
size_t lcp = 0;
size_t to_copy = 0;
if (n <= 0) {
return;
}
first = buf_basename(editor.buffers[idxs[0]]->filename);
if (first == NULL) {
first = NO_NAME;
}
lcp = strnlen(first, FILENAME_MAX);
for (k = 1; k < n; k++) {
cand = buf_basename(editor.buffers[idxs[k]]->filename);
if (cand == NULL) {
cand = NO_NAME;
}
j = 0;
while (j < lcp && first[j] == cand[j]) {
j++;
}
lcp = j;
if (lcp == 0) {
break;
}
}
cur = strlen(buf);
if (lcp > cur) {
to_copy = lcp - cur;
if (cur + to_copy >= bufsz) {
to_copy = bufsz - cur - 1;
}
strncat(buf, first + cur, to_copy);
}
}
static void
buffer_switch_prompt_cb(char *buf, const int16_t key)
{
char msg[80] = {0};
const char *name = NULL;
const char *nm = NULL;
int idxs[64] = {0};
int n = 0;
size_t need = 0;
size_t used = 0;
if (key != TAB_KEY) {
return;
}
n = buffer_collect_prefix_matches(buf, idxs, 64);
if (n <= 0) {
editor_set_status("No matches");
return;
}
if (n == 1) {
name = buf_basename(editor.buffers[idxs[0]]->filename);
if (name == NULL) {
name = NO_NAME;
}
need = strlen(name);
if (need < 128) {
memcpy(buf, name, need);
buf[need] = '\0';
}
editor_set_status("Unique match: %s", name);
return;
}
longest_common_prefix(buf, 128, idxs, n);
msg[0] = '\0';
used = 0;
used += snprintf(msg + used, sizeof(msg) - used, "%d matches: ", n);
for (int i = 0; i < n && used < sizeof(msg) - 1; i++) {
nm = buf_basename(editor.buffers[idxs[i]]->filename);
if (nm == NULL) {
nm = NO_NAME;
}
used += snprintf(msg + used, sizeof(msg) - used, "%s%s",
nm, (i == n - 1 ? "" : ", "));
}
editor_set_status("%s", msg);
}
const char *
buffer_name(buffer *b)
{
if (b && b->filename) {
return buf_basename(b->filename);
}
return NO_NAME;
}
void
buffers_init(void)
{
int idx = 0;
editor.buffers = NULL;
editor.bufcount = 0;
editor.curbuf = 0;
editor.bufcap = 0;
idx = buffer_add_empty();
buffer_switch(idx);
}
static void
buffer_list_resize(void)
{
buffer **newlist = NULL;
if (editor.bufcount == editor.bufcap) {
editor.bufcap = (size_t)cap_growth((int)editor.bufcap, (int)editor.bufcount + 1);
newlist = realloc(editor.buffers, sizeof(buffer *) * editor.bufcap);
assert(newlist != NULL);
editor.buffers = newlist;
}
}
int
buffer_add_empty(void)
{
buffer *buf = NULL;
int idx = 0;
buffer_list_resize();
buf = calloc(1, sizeof(buffer));
assert(buf != NULL);
buf->curx = 0;
buf->cury = 0;
buf->rx = 0;
buf->nrows = 0;
buf->rowoffs = 0;
buf->coloffs = 0;
buf->row = NULL;
buf->filename = NULL;
buf->dirty = 0;
buf->mark_set = 0;
buf->mark_curx = 0;
buf->mark_cury = 0;
editor.buffers[editor.bufcount] = buf;
idx = (int)editor.bufcount;
editor.bufcount++;
return idx;
}
void
buffer_save_current(void)
{
/* No-op: editor no longer mirrors per-buffer fields */
(void)editor;
}
buffer *
buffer_current(void)
{
if (editor.bufcount == 0 || editor.curbuf >= editor.bufcount) {
return NULL;
}
return editor.buffers[editor.curbuf];
}
int
buffer_is_unnamed_and_empty(const buffer *b)
{
if (b == NULL) {
return 0;
}
if (b->filename != NULL) {
return 0;
}
if (b->dirty) {
return 0;
}
if (b->nrows != 0) {
return 0;
}
if (b->row != NULL) {
return 0;
}
return 1;
}
void
buffer_switch(const int idx)
{
buffer *b = NULL;
if (idx < 0 || (size_t)idx >= editor.bufcount) {
return;
}
if (editor.curbuf == (size_t)idx) {
return;
}
b = editor.buffers[idx];
editor.curbuf = (size_t)idx;
editor.dirtyex = 1;
editor_set_status("Switched to buffer %d: %s", editor.curbuf, buffer_name(b));
}
void
buffer_next(void)
{
size_t idx = 0;
if (editor.bufcount <= 1) {
return;
}
idx = (editor.curbuf + 1) % editor.bufcount;
buffer_switch((int)idx);
}
void
buffer_prev(void)
{
size_t idx = 0;
if (editor.bufcount <= 1) {
return;
}
idx = (editor.curbuf == 0) ? (editor.bufcount - 1) : (editor.curbuf - 1);
buffer_switch((int)idx);
}
void
buffer_close_current(void)
{
buffer *b = NULL;
size_t closing = 0;
int target = 0;
int nb = 0;
/* sanity check */
if (editor.bufcount == 0 || editor.curbuf >= editor.bufcount) {
editor_set_status("No buffer to close.");
return;
}
closing = editor.curbuf;
if (editor.bufcount > 1) {
target = (closing > 0) ? (int) (closing - 1) : (int) (closing + 1);
buffer_switch(target);
} else {
nb = buffer_add_empty();
buffer_switch(nb);
}
b = editor.buffers[closing];
if (b) {
if (b->row) {
for (size_t i = 0; i < b->nrows; i++) {
ab_free(&b->row[i]);
}
free(b->row);
}
if (b->filename) {
free(b->filename);
}
free(b);
}
memmove(&editor.buffers[closing], &editor.buffers[closing + 1],
sizeof(buffer *) * (editor.bufcount - closing - 1));
editor.bufcount--;
if (editor.bufcount == 0) {
editor.curbuf = 0;
} else {
if (editor.curbuf > closing) {
editor.curbuf--;
}
}
editor.dirtyex = 1;
editor_set_status("Closed buffer. Now on %s",
buffer_name(editor.buffers[editor.curbuf]));
}
void
buffer_switch_by_name(void)
{
int idxs[64] = {0};
char *name = NULL;
int idx = 0;
int n = 0;
if (editor.bufcount <= 1) {
editor_set_status("No other buffers.");
return;
}
name = editor_prompt("Switch buffer (name, TAB to complete): %s", buffer_switch_prompt_cb);
if (name == NULL) {
return;
}
idx = buffer_find_exact_by_name(name);
if (idx < 0) {
n = buffer_collect_prefix_matches(name, idxs, 64);
if (n == 1) {
idx = idxs[0];
}
}
if (idx >= 0) {
buffer_switch(idx);
} else {
editor_set_status("No open buffer: %s", name);
}
free(name);
}

View File

@@ -1,49 +0,0 @@
#ifndef KE_BUFFER_H
#define KE_BUFFER_H
#include "abuf.h"
typedef struct buffer {
size_t curx, cury;
size_t rx;
size_t nrows;
size_t rowoffs, coloffs;
abuf *row;
char *filename;
int dirty;
int mark_set;
size_t mark_curx, mark_cury;
} buffer;
/* Access current buffer and convenient aliases for file-specific fields */
buffer *buffer_current(void);
#define CURBUF (buffer_current())
#define EROW (CURBUF->row)
#define ENROWS (CURBUF->nrows)
#define ECURX (CURBUF->curx)
#define ECURY (CURBUF->cury)
#define ERX (CURBUF->rx)
#define EROWOFFS (CURBUF->rowoffs)
#define ECOLOFFS (CURBUF->coloffs)
#define EFILENAME (CURBUF->filename)
#define EDIRTY (CURBUF->dirty)
#define EMARK_SET (CURBUF->mark_set)
#define EMARK_CURX (CURBUF->mark_curx)
#define EMARK_CURY (CURBUF->mark_cury)
void buffers_init(void);
int buffer_add_empty(void);
void buffer_save_current(void);
void buffer_switch(int idx);
void buffer_next(void);
void buffer_prev(void);
void buffer_switch_by_name(void);
void buffer_close_current(void);
const char *buffer_name(buffer *b);
int buffer_is_unnamed_and_empty(const buffer *b);
#endif

113
core.c
View File

@@ -1,113 +0,0 @@
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "core.h"
#ifdef INCLUDE_STRNSTR
/*
* Find the first occurrence of find in s, where the search is limited to the
* first slen characters of s.
*/
char *
strnstr(const char *s, const char *find, size_t slen)
{
char c, sc;
size_t len;
if ((c = *find++) != '\0') {
len = strlen(find);
do {
do {
if (slen-- < 1 || (sc = *s++) == '\0')
return (NULL);
} while (sc != c);
if (len > slen)
return (NULL);
} while (strncmp(s, find, len) != 0);
s--;
}
return ((char*)s);
}
#endif
void
swap_size_t(size_t *first, size_t *second)
{
*first ^= *second;
*second ^= *first;
*first ^= *second;
}
int
next_power_of_2(int n)
{
if (n < 2) {
n = 2;
}
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
}
int
cap_growth(int cap, int sz)
{
if (cap == 0) {
cap = INITIAL_CAPACITY;
}
while (cap <= sz) {
cap = next_power_of_2(cap + 1);
}
return cap;
}
size_t
kstrnlen(const char *buf, const size_t max)
{
if (buf == NULL) {
return 0;
}
return strnlen(buf, max);
}
void
kwrite(const int fd, const char* buf, const int len)
{
int wlen = 0;
wlen = write(fd, buf, len);
assert(wlen != -1);
assert(wlen == len);
if (wlen == -1) {
abort();
}
}
void
die(const char* s)
{
kwrite(STDOUT_FILENO, "\x1b[2J", 4);
kwrite(STDOUT_FILENO, "\x1b[H", 3);
perror(s);
exit(1);
}

39
core.h
View File

@@ -1,39 +0,0 @@
#ifndef KE_CORE_H
#define KE_CORE_H
#include <stddef.h>
#define INITIAL_CAPACITY 8
typedef enum key_press {
TAB_KEY = 9,
ESC_KEY = 27,
BACKSPACE = 127,
ARROW_LEFT = 1000,
ARROW_RIGHT = 1001,
ARROW_UP = 1002,
ARROW_DOWN = 1003,
DEL_KEY = 1004,
HOME_KEY = 1005,
END_KEY = 1006,
PG_UP = 1007,
PG_DN = 1008,
} key_press;
#ifndef strnstr
char *strnstr(const char *s, const char *find, size_t slen);
#define INCLUDE_STRNSTR
#endif
void swap_size_t(size_t *first, size_t *second);
int next_power_of_2(int n);
int cap_growth(int cap, int sz);
size_t kstrnlen(const char *buf, size_t max);
void kwrite(int fd, const char *buf, int len);
void die(const char *s);
#endif

View File

@@ -1,31 +1,16 @@
{
lib,
stdenv,
cmake,
installShellFiles,
stdenv,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KE_VERSION \".+\"\\).*" l != null) (throw "KE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "ke";
inherit version;
version = "1.3.3";
src = lib.cleanSource ./.;
nativeBuildInputs = [
cmake
installShellFiles
];
cmakeFlags = [
"-DENABLE_ASAN=on"
"-DCMAKE_BUILD_TYPE=Debug"
];
nativeBuildInputs = [ installShellFiles ];
installPhase = ''
runHook preInstall
@@ -33,8 +18,10 @@ stdenv.mkDerivation {
mkdir -p $out/bin
cp ke $out/bin/
installManPage ../ke.1
runHook postInstall
'';
postInstall = ''
installManPage ke.1
'';
}

237
display.cc Normal file
View File

@@ -0,0 +1,237 @@
#include "display.h"
#include "ke_constants.h"
#include "abuf.h"
#include "erow.h"
#include <termios.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cassert>
// Need access to main.cc structures and functions
extern void erow_update(struct erow *row);
extern int erow_render_to_cursor(struct erow *row, int cx);
// erow definition (needed for accessing row fields)
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
// editor_t definition (needed for display operations)
struct editor_t {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
struct erow *row;
struct erow *killring;
int kill;
int no_kill;
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
time_t msgtm;
};
namespace ke {
void Display::clear(ke::abuf* ab) {
if (ab == nullptr) {
(void)write(STDOUT_FILENO, ESCSEQ "2J", 4);
(void)write(STDOUT_FILENO, ESCSEQ "H", 3);
} else {
ab->append(ESCSEQ "2J", 4);
ab->append(ESCSEQ "H", 3);
}
}
void Display::draw_rows(editor_t* editor, ke::abuf* ab) {
assert(editor->cols >= 0);
char* buf = new char[editor->cols];
int buflen, filerow, padding;
int y;
for (y = 0; y < editor->rows; y++) {
filerow = y + editor->rowoffs;
if (filerow >= editor->nrows) {
if ((editor->nrows == 0) && (y == editor->rows / 3)) {
buflen = snprintf(buf,
editor->cols,
"%s",
KE_VERSION);
padding = (editor->rows - buflen) / 2;
if (padding) {
ab->append("|", 1);
padding--;
}
while (padding--)
ab->append(" ", 1);
ab->append(buf, buflen);
} else {
ab->append("|", 1);
}
} else {
erow_update(&editor->row[filerow]);
buflen = editor->row[filerow].rsize - editor->coloffs;
if (buflen < 0) {
buflen = 0;
}
if (buflen > editor->cols) {
buflen = editor->cols;
}
ab->append(editor->row[filerow].render + editor->coloffs,
buflen);
}
ab->append(ESCSEQ "K", 3);
ab->append("\r\n", 2);
}
delete[] buf;
}
char Display::status_mode_char(int mode) {
switch (mode) {
case MODE_NORMAL:
return 'N';
case MODE_KCOMMAND:
return 'K';
case MODE_ESCAPE:
return 'E';
default:
return '?';
}
}
void Display::draw_status_bar(editor_t* editor, ke::abuf* ab) {
char* status = new char[editor->cols];
char* rstatus = new char[editor->cols];
char* mstatus = new char[editor->cols];
int len, rlen;
len = snprintf(status,
editor->cols,
"%c%cke: %.20s - %d lines",
status_mode_char(editor->mode),
editor->dirty ? '!' : '-',
editor->filename ? editor->filename : "[no file]",
editor->nrows);
if (editor->mark_set) {
snprintf(mstatus,
editor->cols,
" | M: %d, %d ",
editor->mark_curx + 1,
editor->mark_cury + 1);
} else {
snprintf(mstatus, editor->cols, " | M:clear ");
}
rlen = snprintf(rstatus,
editor->cols,
"L%d/%d C%d %s",
editor->cury + 1,
editor->nrows,
editor->curx + 1,
mstatus);
if (len > editor->cols) {
len = editor->cols;
}
ab->append(ESCSEQ "7m", 4);
ab->append(status, len);
while (len < editor->cols) {
if (editor->cols - len == rlen) {
ab->append(rstatus, rlen);
break;
} else {
ab->append(" ", 1);
len++;
}
len++;
}
ab->append(ESCSEQ "m", 3);
ab->append("\r\n", 2);
delete[] status;
delete[] rstatus;
delete[] mstatus;
}
void Display::draw_message_line(editor_t* editor, ke::abuf* ab) {
int len = strlen(editor->msg);
ab->append(ESCSEQ "K", 3);
if (len > editor->cols) {
len = editor->cols;
}
if (len && ((time(nullptr) - editor->msgtm) < MSG_TIMEO)) {
ab->append(editor->msg, len);
}
}
void Display::scroll(editor_t* editor) {
editor->rx = 0;
if (editor->cury < editor->nrows) {
editor->rx = erow_render_to_cursor(
&editor->row[editor->cury],
editor->curx);
}
if (editor->cury < editor->rowoffs) {
editor->rowoffs = editor->cury;
}
if (editor->cury >= editor->rowoffs + editor->rows) {
editor->rowoffs = editor->cury - editor->rows + 1;
}
if (editor->rx < editor->coloffs) {
editor->coloffs = editor->rx;
}
if (editor->rx >= editor->coloffs + editor->cols) {
editor->coloffs = editor->rx - editor->cols + 1;
}
}
void Display::refresh(editor_t* editor) {
char buf[32];
ke::abuf ab;
scroll(editor);
ab.append(ESCSEQ "?25l", 6);
clear(&ab);
draw_rows(editor, &ab);
draw_status_bar(editor, &ab);
draw_message_line(editor, &ab);
snprintf(buf,
sizeof(buf),
ESCSEQ "%d;%dH",
(editor->cury - editor->rowoffs) + 1,
(editor->rx - editor->coloffs) + 1);
ab.append(buf, strnlen(buf, 32));
ab.append(ESCSEQ "?25h", 6);
(void)write(STDOUT_FILENO, ab.data(), ab.size());
}
} // namespace ke

42
display.h Normal file
View File

@@ -0,0 +1,42 @@
#ifndef DISPLAY_HPP
#define DISPLAY_HPP
#include <cstddef>
// Forward declarations
struct editor_t;
namespace ke {
class abuf; // Forward declaration of ke::abuf
/**
* Display class for screen rendering and refresh operations.
*/
class Display {
public:
Display() = default;
~Display() = default;
// Deleted copy constructor and assignment
Display(const Display&) = delete;
Display& operator=(const Display&) = delete;
// Main display operations
static void refresh(editor_t* editor);
static void clear(ke::abuf* ab);
// Drawing operations
static void draw_rows(editor_t* editor, ke::abuf* ab);
static void draw_status_bar(editor_t* editor, ke::abuf* ab);
static void draw_message_line(editor_t* editor, ke::abuf* ab);
// Scrolling
static void scroll(editor_t* editor);
private:
static char status_mode_char(int mode);
};
} // namespace ke
#endif // DISPLAY_HPP

View File

@@ -0,0 +1,115 @@
# Constants Refactoring Documentation
## Overview
This document describes the refactoring of #defines and common constants into a single centralized header file for Kyle's Editor (ke).
## Changes Made
### 1. Created `ke_constants.h`
A new header file was created to centralize all common constants and preprocessor definitions used throughout the project.
**Location**: `/Users/kyle/src/ke2/ke_constants.h`
**Contents**:
- **Version Information**: `KE_VERSION` - Editor version string
- **Terminal Sequences**: `ESCSEQ` - ANSI escape sequence prefix
- **Keyboard Macros**: `CTRL_KEY(key)` - Control key combination macro
- **Display Constants**:
- `TAB_STOP` (8) - Tab stop width
- `MSG_TIMEO` (3) - Message timeout in seconds
- **Memory Management**: `INITIAL_CAPACITY` (64) - Initial buffer capacity
- **Keyboard Modes**:
- `MODE_NORMAL` (0) - Normal editing mode
- `MODE_KCOMMAND` (1) - Command mode (^k commands)
- `MODE_ESCAPE` (2) - Escape key handling mode
- **Kill Ring Operations**:
- `KILLRING_NO_OP` (0) - No operation
- `KILLRING_APPEND` (1) - Append deleted characters
- `KILLRING_PREPEND` (2) - Prepend deleted characters
- `KILLING_SET` (3) - Set killring to deleted character
- `KILLRING_FLUSH` (4) - Clear the killring
- **Legacy Initializers**: `ABUF_INIT` - C struct initializer for append buffer
### 2. Updated `main.c`
- Added `#include "ke_constants.h"` at the top with other includes
- Removed all duplicate #define statements that were moved to the constants header
- Eliminated the duplicate `TAB_STOP` definition (was defined twice on lines 36 and 50)
- Cleaned up approximately 30+ lines of redundant definitions
### 3. Updated `erow.cpp`
- Added `#include "ke_constants.h"` with extern "C" linkage for C++ compatibility
- Removed local `constexpr int TAB_STOP = 8` definition
- Now uses the centralized `TAB_STOP` constant from `ke_constants.h`
## Benefits
1. **Single Source of Truth**: All constants are now defined in one location, making them easier to maintain and modify
2. **Eliminated Duplication**: Removed duplicate definitions (e.g., TAB_STOP was defined twice in main.c)
3. **Better Organization**: Constants are grouped logically by category with clear comments
4. **Cross-Language Compatibility**: Header works with both C (main.c) and C++ (erow.cpp) code
5. **Easier Maintenance**: Future changes to constants only need to be made in one place
6. **Improved Readability**: Cleaner source files with less clutter from #define statements
## Build System Compatibility
Both build systems have been tested and verified to work correctly:
### Makefile Build
```bash
make clean
make
```
Successfully compiles all files (main.c, abuf.cpp, erow.cpp) and links the executable.
### CMake Build
```bash
cd cmake-build-debug
cmake --build . --clean-first
```
Successfully builds the project with all components.
## Header Guard
The header uses standard include guards:
```c
#ifndef KE_CONSTANTS_H
#define KE_CONSTANTS_H
...
#endif /* KE_CONSTANTS_H */
```
## C/C++ Interoperability
When including in C++ files, use extern "C" linkage:
```cpp
extern "C" {
#include "ke_constants.h"
}
```
This ensures proper linkage and prevents name mangling issues.
## Future Considerations
1. **Additional Constants**: As the project grows, any new constants should be added to `ke_constants.h` rather than being scattered across source files
2. **Namespacing**: For C++ constants, consider creating a dedicated C++ header with namespaced constants
3. **Type Safety**: For C++ code, consider using `constexpr` or `inline constexpr` variables in a C++ header for better type safety
4. **Documentation**: Keep this document updated as new constants are added or modified
## Testing
All compilation tests passed:
- ✅ Makefile build: Clean compilation with no errors or warnings
- ✅ CMake build: Clean compilation with no errors or warnings
- ✅ All source files (C and C++) successfully include and use the constants
- ✅ No duplicate definition errors
- ✅ Proper linkage between C and C++ components
## Related Files
- `ke_constants.h` - The new constants header (created)
- `main.c` - Updated to use new header (modified)
- `erow.cpp` - Updated to use new header (modified)
- `Makefile` - No changes required (working)
- `CMakeLists.txt` - No changes required (working)
## Date
November 24, 2025

109
docs/CPP17_CONVERSION.md Normal file
View File

@@ -0,0 +1,109 @@
# C++17 Conversion Documentation
## Overview
This document describes the initial conversion of the ke2 project from C to C++17 standards, starting with the `abuf` and `erow` structures as requested.
## Converted Components
### 1. abuf (Append Buffer)
**Original C Structure (main.c:69-73):**
```c
struct abuf {
char *b;
int len;
int cap;
};
```
**C++17 Implementation (abuf.hpp, abuf.cpp):**
- Converted to a modern C++ class using RAII principles
- Uses `std::string` as the internal buffer for automatic memory management
- Implements move semantics (deleted copy operations)
- Uses C++17 features:
- `std::string_view` for efficient string parameters
- `[[nodiscard]]` attributes for better safety
- `noexcept` specifications where appropriate
- No manual memory management required - destructor automatically handled by std::string
**Key Improvements:**
- Automatic memory cleanup (RAII)
- Exception-safe
- No memory leaks possible
- More efficient with move semantics
- Type-safe with modern C++ idioms
### 2. erow (Editor Row)
**Original C Structure (main.c:79-87):**
```c
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
```
**C++17 Implementation (erow.hpp, erow.cpp):**
- Converted to a C++ class with proper encapsulation
- Uses `std::string` for both `line` and `render` buffers
- Implements move semantics (deleted copy operations)
- All manual memory management replaced with RAII
- Retains all original functionality:
- UTF-8 character handling with `mbrtowc` and `wcwidth`
- Tab expansion (TAB_STOP = 8)
- Control character rendering as `\xx`
- Cursor-to-render and render-to-cursor position conversions
**Key Improvements:**
- Automatic memory cleanup for both line and render buffers
- No malloc/free/realloc needed
- Exception-safe operations
- Better encapsulation with private members
- Modern C++ method naming conventions
## Build System Updates
### Makefile
- Added C++ compiler support (g++)
- Added CXXFLAGS with `-std=c++17`
- Modified build rules to compile C and C++ separately
- Updated to link with C++ compiler
- Enhanced clean target to remove object files
### CMakeLists.txt
- Changed project language from `C` to `C CXX`
- Added `CMAKE_CXX_STANDARD 17` and `CMAKE_CXX_STANDARD_REQUIRED ON`
- Added C++ compiler flags matching C flags
- Updated executable target to include `abuf.cpp` and `erow.cpp`
- Maintained backward compatibility with existing C code (main.c)
## Compilation Testing
Both build systems have been tested and verified:
- ✅ Makefile: Successfully compiles with `make`
- ✅ CMake: Successfully configures and builds
- ✅ Object files generated correctly for all C++ sources
- ✅ No compilation warnings or errors
## C++17 Features Used
1. **std::string** - Modern string class with automatic memory management
2. **std::string_view** - Efficient non-owning string references (C++17)
3. **[[nodiscard]]** - Compiler warnings for ignored return values (C++17)
4. **noexcept** - Exception specifications for optimization
5. **default/delete** - Explicit control over special member functions
6. **Move semantics** - Efficient resource transfer
7. **constexpr** - Compile-time constants (TAB_STOP)
## Next Steps
The project now has a foundation for further C++17 conversion:
- Additional structures can be converted following the same patterns
- Consider converting more C code to C++ classes
- Potential for using more STL containers (std::vector for dynamic arrays)
- Opportunity to use smart pointers for other memory management
- Can leverage more C++17 features (std::optional, structured bindings, etc.)
## Compatibility
- The new C++ code coexists with existing C code (main.c)
- Both can be compiled together and linked successfully
- The conversion maintains the original functionality
- No changes required to existing C code at this stage

109
editor.c
View File

@@ -1,109 +0,0 @@
/* editor.c - editor-wide state and functions */
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "abuf.h"
#include "buffer.h"
#include "core.h"
#include "editor.h"
#include "term.h"
/*
* Global editor instance
*/
struct editor editor = {
.cols = 0,
.rows = 0,
.mode = 0,
.killring = NULL,
.kill = 0,
.no_kill = 0,
.dirtyex = 0,
.uarg = 0,
.ucount = 0,
.msgtm = 0,
.buffers = NULL,
.bufcount = 0,
.curbuf = 0,
.bufcap = 0,
};
void
editor_set_status(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsnprintf(editor.msg, sizeof(editor.msg), fmt, ap);
va_end(ap);
editor.msgtm = time(NULL);
}
void
init_editor(void)
{
editor.cols = 0;
editor.rows = 0;
if (get_winsz(&editor.rows, &editor.cols) == -1) {
die("can't get window size");
}
editor.rows--; /* status bar */
editor.rows--; /* message line */
/* don't clear out the kill ring:
* killing / yanking across files is helpful, and killring
* is initialized to NULL at program start.
*/
editor.kill = 0;
editor.no_kill = 0;
editor.msg[0] = '\0';
editor.msgtm = 0;
/* initialize buffer system on first init */
if (editor.buffers == NULL && editor.bufcount == 0) {
editor.bufcap = 0;
buffers_init();
}
}
void
reset_editor(void)
{
/* Reset the current buffer's contents/state. */
buffer *b = buffer_current();
if (b == NULL) {
return;
}
if (b->row) {
for (size_t i = 0; i < b->nrows; i++) {
ab_free(&b->row[i]);
}
free(b->row);
}
b->row = NULL;
b->nrows = 0;
b->rowoffs = 0;
b->coloffs = 0;
b->rx = 0;
b->curx = 0;
b->cury = 0;
if (b->filename) {
free(b->filename);
b->filename = NULL;
}
b->dirty = 0;
b->mark_set = 0;
b->mark_curx = 0;
b->mark_cury = 0;
}

View File

@@ -1,35 +0,0 @@
#ifndef KE_EDITOR_H
#define KE_EDITOR_H
#include <termios.h>
#include <time.h>
#include "abuf.h"
#include "buffer.h"
struct editor {
size_t rows, cols;
int mode;
abuf *killring;
int kill; /* KILL CHAIN (\m/) */
int no_kill; /* don't kill in delete_row */
int dirtyex;
char msg[80];
int uarg, ucount; /* C-u support */
time_t msgtm;
struct buffer **buffers; /* array of buffers */
size_t bufcount; /* number of buffers */
size_t curbuf; /* current buffer index */
size_t bufcap; /* current buffer capacity */
};
extern struct editor editor;
void editor_set_status(const char *fmt, ...);
void init_editor(void);
void reset_editor(void);
#endif /* KE_EDITOR_H */

214
erow.cc Normal file
View File

@@ -0,0 +1,214 @@
#include "erow.h"
#include <cstring>
#include <cwchar>
#include <cassert>
#include "ke_constants.h"
namespace ke {
// Constructors
erow::erow(std::size_t initial_capacity) {
line_.reserve(initial_capacity);
}
erow::erow(const char* text, std::size_t len) {
line_.assign(text, len);
}
// Set line content
void erow::set_line(const char* data, std::size_t len) {
line_.assign(data, len);
}
// Append string to the line
void erow::append_string(const char* s, std::size_t len) {
line_.append(s, len);
}
// Insert character at position
void erow::insert_char(std::size_t at, char c) {
if (at > line_.size()) {
at = line_.size();
}
line_.insert(at, 1, c);
}
// Delete character at position
void erow::delete_char(std::size_t at) {
if (at < line_.size()) {
line_.erase(at, 1);
}
}
// Helper function for nibble to hex conversion
char erow::nibble_to_hex(char c) {
c &= 0xf;
if (c < 10) {
return static_cast<char>('0' + c);
}
return static_cast<char>('A' + (c - 10));
}
// Update the render string based on the line content
void erow::update() {
int tabs = 0;
int ctrl = 0;
// Count tabs and control characters
for (std::size_t j = 0; j < line_.size(); ++j) {
if (line_[j] == '\t') {
++tabs;
} else if (static_cast<unsigned char>(line_[j]) < 0x20) {
++ctrl;
}
}
// Allocate render buffer
render_.clear();
render_.reserve(line_.size() + (tabs * (TAB_STOP - 1)) + (ctrl * 3) + 1);
// Build the render string
for (std::size_t j = 0; j < line_.size(); ++j) {
if (line_[j] == '\t') {
// Expand tabs to spaces
do {
render_.push_back(' ');
} while ((render_.size() % TAB_STOP) != 0);
} else if (static_cast<unsigned char>(line_[j]) < 0x20) {
// Render control characters as \xx
render_.push_back('\\');
render_.push_back(nibble_to_hex(line_[j] >> 4));
render_.push_back(nibble_to_hex(line_[j] & 0x0f));
} else {
// Leave UTF-8 multibyte bytes untouched
render_.push_back(line_[j]);
}
}
}
// Convert cursor position to render position
int erow::render_to_cursor(int cx) const {
int rx = 0;
std::size_t j = 0;
wchar_t wc;
std::mbstate_t st{};
while (j < static_cast<std::size_t>(cx) && j < line_.size()) {
unsigned char b = static_cast<unsigned char>(line_[j]);
if (b == '\t') {
rx += (TAB_STOP - 1) - (rx % TAB_STOP);
++rx;
++j;
continue;
}
if (b < 0x20) {
// Render as \xx -> width 3
rx += 3;
++j;
continue;
}
std::size_t rem = line_.size() - j;
std::size_t n = std::mbrtowc(&wc, &line_[j], rem, &st);
if (n == static_cast<std::size_t>(-2)) {
// Incomplete sequence at end; treat one byte
rx += 1;
j += 1;
std::memset(&st, 0, sizeof(st));
} else if (n == static_cast<std::size_t>(-1)) {
// Invalid byte; consume one and reset state
rx += 1;
j += 1;
std::memset(&st, 0, sizeof(st));
} else if (n == 0) {
// Null character
rx += 0;
j += 1;
} else {
int w = wcwidth(wc);
if (w < 0) {
w = 1; // Non-printable -> treat as width 1
}
rx += w;
j += n;
}
}
return rx;
}
// Convert render position to cursor position
int erow::cursor_to_render(int rx) const {
int cur_rx = 0;
std::size_t j = 0;
wchar_t wc;
std::mbstate_t st{};
while (j < line_.size()) {
unsigned char b = static_cast<unsigned char>(line_[j]);
if (b == '\t') {
int tab_width = (TAB_STOP - 1) - (cur_rx % TAB_STOP) + 1;
if (cur_rx + tab_width > rx) {
break;
}
cur_rx += tab_width;
++j;
continue;
}
if (b < 0x20) {
// Control char width is 3
if (cur_rx + 3 > rx) {
break;
}
cur_rx += 3;
++j;
continue;
}
std::size_t rem = line_.size() - j;
std::size_t n = std::mbrtowc(&wc, &line_[j], rem, &st);
std::size_t adv = 1;
int w = 1;
if (n == static_cast<std::size_t>(-2)) {
// Incomplete sequence
adv = 1;
w = 1;
std::memset(&st, 0, sizeof(st));
} else if (n == static_cast<std::size_t>(-1)) {
// Invalid byte
adv = 1;
w = 1;
std::memset(&st, 0, sizeof(st));
} else if (n == 0) {
// Null character
adv = 1;
w = 0;
} else {
adv = n;
w = wcwidth(wc);
if (w < 0) {
w = 1;
}
}
if (cur_rx + w > rx) {
break;
}
cur_rx += w;
j += adv;
}
return static_cast<int>(j);
}
} // namespace ke

76
erow.h Normal file
View File

@@ -0,0 +1,76 @@
#ifndef EROW_HPP
#define EROW_HPP
#include <string>
#include <memory>
#include <cstddef>
#include <cstdint>
namespace ke {
/**
* Editor row class representing a line of text with rendering information.
* C++17 implementation with RAII memory management.
*/
class erow {
public:
// Constructors
erow() noexcept = default;
explicit erow(std::size_t initial_capacity);
erow(const char* text, std::size_t len);
// Deleted copy constructor and assignment (use move semantics)
erow(const erow&) = delete;
erow& operator=(const erow&) = delete;
// Move constructor and assignment
erow(erow&&) noexcept = default;
erow& operator=(erow&&) noexcept = default;
// Destructor (automatic cleanup via std::string)
~erow() = default;
// Core operations
void update();
void insert_char(std::size_t at, char c);
void delete_char(std::size_t at);
void append_string(const char* s, std::size_t len);
// Cursor/render position conversions
[[nodiscard]] int render_to_cursor(int cx) const;
[[nodiscard]] int cursor_to_render(int rx) const;
// Accessors for line data
[[nodiscard]] const char* line_data() const noexcept { return line_.data(); }
[[nodiscard]] char* line_data() noexcept { return line_.data(); }
[[nodiscard]] std::size_t line_size() const noexcept { return line_.size(); }
[[nodiscard]] std::size_t line_capacity() const noexcept { return line_.capacity(); }
// Accessors for render data
[[nodiscard]] const char* render_data() const noexcept { return render_.data(); }
[[nodiscard]] std::size_t render_size() const noexcept { return render_.size(); }
// Direct access to strings (for compatibility)
[[nodiscard]] std::string& line() noexcept { return line_; }
[[nodiscard]] const std::string& line() const noexcept { return line_; }
[[nodiscard]] const std::string& render() const noexcept { return render_; }
// Resize operations
void resize_line(std::size_t new_size) { line_.resize(new_size); }
void reserve_line(std::size_t new_cap) { line_.reserve(new_cap); }
// Set line content
void set_line(const char* data, std::size_t len);
void set_line(std::string&& data) noexcept { line_ = std::move(data); }
private:
std::string line_; // The actual line content
std::string render_; // The rendered version (with tabs expanded, etc.)
// Helper for nibble to hex conversion
static char nibble_to_hex(char c);
};
} // namespace ke
#endif // EROW_HPP

180
file_io.cc Normal file
View File

@@ -0,0 +1,180 @@
#include "file_io.h"
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <ctime>
// Need access to main.cc structures and functions
extern void erow_insert(int at, const char *s, int len);
extern void reset_editor();
extern void editor_set_status(const char *fmt, ...);
extern void die(const char *s);
extern char *editor_prompt(const char *, void (*cb)(char *, int16_t));
// erow definition
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
// editor_t definition
struct editor_t {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
struct erow *row;
struct erow *killring;
int kill;
int no_kill;
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
time_t msgtm;
};
namespace ke {
void FileIO::open_file(editor_t* editor, const char* filename) {
char* line = nullptr;
size_t linecap = 0;
ssize_t linelen;
FILE* fp = nullptr;
reset_editor();
editor->filename = strdup(filename);
assert(editor->filename != nullptr);
editor->dirty = 0;
if ((fp = fopen(filename, "r")) == nullptr) {
if (errno == ENOENT) {
editor_set_status("[new file]");
return;
}
die("fopen");
}
while ((linelen = getline(&line, &linecap, fp)) != -1) {
if (linelen != -1) {
while ((linelen > 0) && ((line[linelen - 1] == '\r') ||
(line[linelen - 1] == '\n'))) {
linelen--;
}
erow_insert(editor->nrows, line, linelen);
}
}
free(line);
line = nullptr;
fclose(fp);
}
char* FileIO::rows_to_buffer(editor_t* editor, int* buflen) {
int len = 0;
int j;
char* buf = nullptr;
char* p = nullptr;
for (j = 0; j < editor->nrows; j++) {
/* extra byte for newline */
len += editor->row[j].size + 1;
}
if (len == 0) {
return nullptr;
}
*buflen = len;
buf = static_cast<char*>(malloc(len));
assert(buf != nullptr);
p = buf;
for (j = 0; j < editor->nrows; j++) {
memcpy(p, editor->row[j].line, editor->row[j].size);
p += editor->row[j].size;
*p++ = '\n';
}
return buf;
}
int FileIO::save_file(editor_t* editor) {
int fd = -1;
int len;
int status = 1; /* will be used as exit code */
char* buf;
if (!editor->dirty) {
editor_set_status("No changes to save.");
return 0;
}
if (editor->filename == nullptr) {
editor->filename = editor_prompt("Filename: %s", nullptr);
if (editor->filename == nullptr) {
editor_set_status("Save aborted.");
return 0;
}
}
buf = rows_to_buffer(editor, &len);
if ((fd = open(editor->filename, O_RDWR | O_CREAT, 0644)) == -1) {
goto save_exit;
}
if (-1 == ftruncate(fd, len)) {
goto save_exit;
}
if (len == 0) {
status = 0;
goto save_exit;
}
if ((ssize_t) len != write(fd, buf, len)) {
goto save_exit;
}
status = 0;
save_exit:
if (fd)
close(fd);
if (buf) {
free(buf);
buf = nullptr;
}
if (status != 0) {
buf = strerror(errno);
editor_set_status("Error writing %s: %s",
editor->filename,
buf);
} else {
editor_set_status("Wrote %d bytes to %s.",
len,
editor->filename);
editor->dirty = 0;
}
return status;
}
} // namespace ke

29
file_io.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef FILE_IO_HPP
#define FILE_IO_HPP
// Forward declaration
struct editor_t;
namespace ke {
/**
* File I/O class for reading and writing files.
*/
class FileIO {
public:
FileIO() = default;
~FileIO() = default;
// Deleted copy constructor and assignment
FileIO(const FileIO&) = delete;
FileIO& operator=(const FileIO&) = delete;
// File operations
static void open_file(editor_t* editor, const char* filename);
static int save_file(editor_t* editor);
static char* rows_to_buffer(editor_t* editor, int* buflen);
};
} // namespace ke
#endif // FILE_IO_HPP

122
input_handler.cc Normal file
View File

@@ -0,0 +1,122 @@
#include "input_handler.h"
#include "ke_constants.h"
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
namespace ke {
// Key codes for special keys
enum KeyPress {
TAB_KEY = 9,
ESC_KEY = 27,
BACKSPACE = 127,
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
DEL_KEY,
HOME_KEY,
END_KEY,
PG_UP,
PG_DN,
};
int16_t InputHandler::get_keypress() {
char seq[3];
/* read raw byte so UTF-8 bytes (>=0x80) are not sign-extended */
unsigned char uc = 0;
int16_t c;
if (read(STDIN_FILENO, &uc, 1) == -1) {
perror("get_keypress:read");
exit(1);
}
c = (int16_t) uc;
if (c == 0x1b) {
if (read(STDIN_FILENO, &seq[0], 1) != 1)
return c;
if (read(STDIN_FILENO, &seq[1], 1) != 1)
return c;
if (seq[0] == '[') {
if (seq[1] < 'A') {
if (read(STDIN_FILENO, &seq[2], 1) != 1)
return c;
if (seq[2] == '~') {
switch (seq[1]) {
case '1':
return HOME_KEY;
case '3':
return DEL_KEY;
case '4':
return END_KEY;
case '5':
return PG_UP;
case '6':
return PG_DN;
case '7':
return HOME_KEY;
case '8':
return END_KEY;
}
}
} else {
switch (seq[1]) {
case 'A':
return ARROW_UP;
case 'B':
return ARROW_DOWN;
case 'C':
return ARROW_RIGHT;
case 'D':
return ARROW_LEFT;
case 'F':
return END_KEY;
case 'H':
return HOME_KEY;
default:
/* nada */ ;
}
}
} else if (seq[0] == 'O') {
switch (seq[1]) {
case 'F':
return END_KEY;
case 'H':
return HOME_KEY;
}
}
return 0x1b;
}
return c;
}
bool InputHandler::is_arrow_key(int16_t c) {
switch (c) {
case ARROW_DOWN:
case ARROW_LEFT:
case ARROW_RIGHT:
case ARROW_UP:
case CTRL_KEY('p'):
case CTRL_KEY('n'):
case CTRL_KEY('f'):
case CTRL_KEY('b'):
case CTRL_KEY('a'):
case CTRL_KEY('e'):
case END_KEY:
case HOME_KEY:
case PG_DN:
case PG_UP:
return true;
}
return false;
}
} // namespace ke

29
input_handler.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef INPUT_HANDLER_HPP
#define INPUT_HANDLER_HPP
#include <cstdint>
namespace ke {
/**
* Input handler class for reading and processing keyboard input.
*/
class InputHandler {
public:
InputHandler() = default;
~InputHandler() = default;
// Deleted copy constructor and assignment
InputHandler(const InputHandler&) = delete;
InputHandler& operator=(const InputHandler&) = delete;
// Read a keypress from stdin
static int16_t get_keypress();
// Check if a key code is an arrow/navigation key
static bool is_arrow_key(int16_t c);
};
} // namespace ke
#endif // INPUT_HANDLER_HPP

42
ke.1
View File

@@ -22,21 +22,11 @@ grandeur. Many commands work with and without control; for example,
saving a file can be done with either C-k s or C-k C-s. Other commands work
with ESC or CTRL.
.Sh K-COMMANDS
k-command mode can be exited with ESC or C-g.
.Bl -tag -width xxxxxxxxxxxx -offset indent
.It C-k BACKSPACE
Delete from the cursor to the beginning of the line.
.It C-k SPACE
Toggle the mark.
.It C-k -
If the mark is set, unindent the region.
.It C-k =
If the mark is set, indent the region.
.It C-k b
Switch to a buffer.
.It C-k c
Close the current buffer. If no other buffers are open, an empty
buffer will be opened. To exit, use C-k q.
.It C-k d
Delete from the cursor to the end of the line.
.It C-k C-d
@@ -44,52 +34,34 @@ Delete the entire line.
.It C-k e
Edit a new file. Also C-k C-e.
.It C-k f
Flush the kill ring.
Incremental find.
.It C-k g
Go to a specific line.
.It C-k j
Jump to the mark.
Go to a specific line. Also C-k C-g.
.It C-k l
List the number of lines of code in a saved file.
.It C-k m
Run make(1).
.It C-k p
Switch to the next buffer.
.It C-k q
Exit the editor. If the file has unsaved changes,
a warning will be printed; a second C-k q will exit.
.It C-k C-q
Immediately exit the editor.
.It C-k C-r
Reload the current buffer from disk.
exit the editor. Also C-k C-q.
.It C-k s
Save the file, prompting for a filename if needed. Also C-k C-s.
.It C-k u
Undo changes (not implemented; marking this k-command as taken).
.It C-k U
Redo changes (not implemented; marking this k-command as taken).
save the file, prompting for a filename if needed. Also C-k C-s.
.It C-k x
save the file and exit. Also C-k C-x.
.It C-k y
Yank the kill ring.
Yank the killring.
.It C-k \[char92]
Dump core.
.El
.Sh OTHER KEYBINDINGS
.Bl -tag -width xxxxxxxxxxxx -offset indent
.It C-g
In general, C-g cancels an operation.
.It C-l
Refresh the display.
.It C-s
Incremental find.
.It C-u
Universal argument. C-u followed by numbers will repeat an
operation n times.
.It C-w
Kill the region if the mark is set.
.It C-y
Yank the kill ring.
Yank the killring.
.It ESC BACKSPACE
Delete the previous word.
.It ESC b
@@ -99,7 +71,7 @@ Delete the next word.
.It ESC f
Move to the next word.
.It ESC w
Save the region (if the mark is set) to the kill ring.
Save the region (if the mark is set) to the killring.
.It
.El
.Sh FIND

44
ke_constants.h Normal file
View File

@@ -0,0 +1,44 @@
/*
* ke_constants.h
*
* Common constants and defines for Kyle's Editor (ke)
* Refactored from main.c and other source files for centralized maintenance.
*/
#ifndef KE_CONSTANTS_H
#define KE_CONSTANTS_H
/* Version information */
#ifndef KE_VERSION
#define KE_VERSION "ke dev build"
#endif
/* Terminal escape sequences */
#define ESCSEQ "\x1b["
/* Keyboard and control key macros */
#define CTRL_KEY(key) ((key)&0x1f)
/* Display and rendering constants */
#define TAB_STOP 8
#define MSG_TIMEO 3
/* Memory management constants */
#define INITIAL_CAPACITY 64
/* Keyboard input modes */
#define MODE_NORMAL 0
#define MODE_KCOMMAND 1
#define MODE_ESCAPE 2
/* Kill ring operations */
#define KILLRING_NO_OP 0 /* don't touch the killring */
#define KILLRING_APPEND 1 /* append deleted chars */
#define KILLRING_PREPEND 2 /* prepend deleted chars */
#define KILLING_SET 3 /* set killring to deleted char */
#define KILLRING_FLUSH 4 /* clear the killring */
/* Legacy C struct initializers (for compatibility with main.c) */
#define ABUF_INIT {NULL, 0, 0}
#endif /* KE_CONSTANTS_H */

262
killring.cc Normal file
View File

@@ -0,0 +1,262 @@
#include "killring.h"
#include <termios.h>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <ctime>
// Need access to main.cc structures and functions
extern int erow_init(struct erow *row, int len);
extern void erow_update(struct erow *row);
extern void erow_free(struct erow *row);
extern void editor_set_status(const char *fmt, ...);
extern void insertch(int16_t c);
extern void newline();
extern void move_cursor(int16_t c);
extern int cursor_at_eol();
extern void swap_int(int *a, int *b);
extern void deletech(uint8_t op);
// erow definition
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
// editor_t definition
struct editor_t {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
struct erow *row;
struct erow *killring;
int kill;
int no_kill;
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
time_t msgtm;
};
namespace ke {
void Killring::flush(editor_t* editor) {
if (editor->killring != nullptr) {
erow_free(editor->killring);
free(editor->killring);
editor->killring = nullptr;
}
}
void Killring::yank(editor_t* editor) {
if (editor->killring == nullptr) {
return;
}
/*
* Insert killring contents at the cursor without clearing the ring.
* Interpret '\n' as an actual newline() rather than inserting a raw 0x0A
* byte, so yanked content preserves lines correctly.
*/
for (int i = 0; i < editor->killring->size; i++) {
unsigned char ch = (unsigned char) editor->killring->line[i];
if (ch == '\n') {
newline();
} else {
insertch(ch);
}
}
}
void Killring::start_with_char(editor_t* editor, unsigned char ch) {
erow* row = nullptr;
if (editor->killring != nullptr) {
erow_free(editor->killring);
free(editor->killring);
editor->killring = nullptr;
}
editor->killring = static_cast<erow*>(malloc(sizeof(erow)));
assert(editor->killring != nullptr);
assert(erow_init(editor->killring, 0) == 0);
/* append one char to empty killring without affecting editor.dirty */
row = editor->killring;
row->line = static_cast<char*>(realloc(row->line, row->size + 2));
assert(row->line != nullptr);
row->line[row->size] = ch;
row->size++;
row->line[row->size] = '\0';
erow_update(row);
}
void Killring::append_char(editor_t* editor, unsigned char ch) {
erow* row = nullptr;
if (editor->killring == nullptr) {
start_with_char(editor, ch);
return;
}
row = editor->killring;
row->line = static_cast<char*>(realloc(row->line, row->size + 2));
assert(row->line != nullptr);
row->line[row->size] = ch;
row->size++;
row->line[row->size] = '\0';
erow_update(row);
}
void Killring::prepend_char(editor_t* editor, unsigned char ch) {
if (editor->killring == nullptr) {
start_with_char(editor, ch);
return;
}
erow* row = editor->killring;
row->line = static_cast<char*>(realloc(row->line, row->size + 2));
assert(row->line != nullptr);
memmove(&row->line[1], &row->line[0], row->size + 1);
row->line[0] = ch;
row->size++;
erow_update(row);
}
void Killring::toggle_markset(editor_t* editor) {
if (editor->mark_set) {
editor->mark_set = 0;
editor_set_status("Mark cleared.");
return;
}
editor->mark_set = 1;
editor->mark_curx = editor->curx;
editor->mark_cury = editor->cury;
editor_set_status("Mark set.");
}
int Killring::cursor_after_mark(editor_t* editor) {
if (editor->mark_cury < editor->cury) {
return 1;
}
if (editor->mark_cury > editor->cury) {
return 0;
}
return editor->curx >= editor->mark_curx;
}
int Killring::count_chars_from_cursor_to_mark(editor_t* editor) {
int count = 0;
int curx = editor->curx;
int cury = editor->cury;
int markx = editor->mark_curx;
int marky = editor->mark_cury;
if (!cursor_after_mark(editor)) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor->curx = markx;
editor->cury = marky;
while (editor->cury != cury) {
while (!cursor_at_eol()) {
move_cursor(1000 + 3); // ARROW_RIGHT
count++;
}
move_cursor(1000 + 3); // ARROW_RIGHT
count++;
}
while (editor->curx != curx) {
count++;
move_cursor(1000 + 3); // ARROW_RIGHT
}
return count;
}
void Killring::kill_region(editor_t* editor) {
int curx = editor->curx;
int cury = editor->cury;
int markx = editor->mark_curx;
int marky = editor->mark_cury;
if (!editor->mark_set) {
return;
}
/* kill the current killring */
flush(editor);
if (!cursor_after_mark(editor)) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor->curx = markx;
editor->cury = marky;
while (editor->cury != cury) {
while (!cursor_at_eol()) {
append_char(editor, editor->row[editor->cury].line[editor->curx]);
move_cursor(1000 + 3); // ARROW_RIGHT
}
append_char(editor, '\n');
move_cursor(1000 + 3); // ARROW_RIGHT
}
while (editor->curx != curx) {
append_char(editor, editor->row[editor->cury].line[editor->curx]);
move_cursor(1000 + 3); // ARROW_RIGHT
}
editor_set_status("Region killed.");
}
void Killring::delete_region(editor_t* editor) {
int count = count_chars_from_cursor_to_mark(editor);
int killed = 0;
int curx = editor->curx;
int cury = editor->cury;
int markx = editor->mark_curx;
int marky = editor->mark_cury;
if (!editor->mark_set) {
return;
}
if (!cursor_after_mark(editor)) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor->curx = markx;
editor->cury = marky;
while (killed < count) {
deletech(0); // KILLRING_NO_OP
killed++;
}
editor->kill = 1;
editor_set_status("Region killed.");
}
} // namespace ke

44
killring.h Normal file
View File

@@ -0,0 +1,44 @@
#ifndef KILLRING_HPP
#define KILLRING_HPP
#include <cstdint>
// Forward declarations
struct editor_t;
namespace ke {
/**
* Killring class for cut/paste (kill/yank) operations.
*/
class Killring {
public:
Killring() = default;
~Killring() = default;
// Deleted copy constructor and assignment
Killring(const Killring&) = delete;
Killring& operator=(const Killring&) = delete;
// Killring operations
static void flush(editor_t* editor);
static void yank(editor_t* editor);
static void start_with_char(editor_t* editor, unsigned char ch);
static void append_char(editor_t* editor, unsigned char ch);
static void prepend_char(editor_t* editor, unsigned char ch);
// Mark operations
static void toggle_markset(editor_t* editor);
static int cursor_after_mark(editor_t* editor);
// Region operations
static void kill_region(editor_t* editor);
static void delete_region(editor_t* editor);
private:
static int count_chars_from_cursor_to_mark(editor_t* editor);
};
} // namespace ke
#endif // KILLRING_HPP

2915
main.c

File diff suppressed because it is too large Load Diff

2531
main.cc Normal file

File diff suppressed because it is too large Load Diff

206
scratch.c
View File

@@ -1,206 +0,0 @@
/*
* scratch.c - ideas in progress
*/
#include "buffer.h"
#include "abuf.h"
#include <ctype.h>
#include <string.h>
#define REFLOW_MARGIN 72
void
reflow_region(void)
{
int start_row, end_row, i, col, wlen, this_len;
abuf *row;
struct abuf buf = ABUF_INIT;
struct abuf out = ABUF_INIT;
int in_paragraph = 0;
int indent_len = 0;
char indent[REFLOW_MARGIN + 1];
char word[REFLOW_MARGIN + 1];
char *e = NULL;
char *p = NULL;
char *s = NULL;
if (EMARK_SET) {
if (EMARK_CURY < ECURY ||
(EMARK_CURY == ECURY &&
EMARK_CURX < ECURX)) {
start_row = EMARK_CURY;
end_row = ECURY;
} else {
start_row = ECURY;
end_row = EMARK_CURY;
}
} else {
start_row = end_row = ECURY;
while (start_row > 0 && EROW[start_row - 1].size > 0) {
start_row--;
}
while (end_row < ENROWS - 1 &&
EROW[end_row + 1].size > 0) {
end_row++;
}
}
if (start_row >= ENROWS) {
return;
}
if (end_row >= ENROWS) {
end_row = ENROWS - 1;
}
for (i = start_row; i <= end_row; i++) {
row = &EROW[i];
if (row->size == 0) {
if (in_paragraph) {
ab_append(&buf, "\n", 1);
in_paragraph = 0;
}
ab_append(&buf, "\n", 1);
continue;
}
if (!in_paragraph) {
indent_len = 0;
while (indent_len < (int)row->size &&
(row->b[indent_len] == ' ' ||
row->b[indent_len] == '\t')) {
indent[indent_len] = row->b[indent_len], indent_len++;
}
indent[indent_len] = '\0';
in_paragraph = 1;
}
ab_append(&buf, row->b + indent_len, row->size - indent_len);
ab_append(&buf, " ", 1);
}
if (in_paragraph) {
ab_append(&buf, "\n", 1);
}
p = buf.b;
col = 0;
while (p != NULL && *p != '\0') {
while (*p && isspace((unsigned char)*p) &&
*p != '\n') {
p++;
}
if (*p == '\0') {
break;
}
wlen = 0;
while (*p && !isspace((unsigned char)*p)) {
if (wlen < REFLOW_MARGIN)
word[wlen++] = *p;
p++;
}
word[wlen] = '\0';
if (*p == '\n' && (p[1] == '\n' || p[1] == '\0')) {
ab_append(&out, "\n", 1); /* flush */
col = 0;
p++; /* consume the extra \n */
continue;
}
this_len = wlen;
if (col > 0) {
this_len++; /* space before word */
}
if (col == 0) {
ab_append(&out, indent, indent_len);
col = indent_len;
}
if (col + this_len > REFLOW_MARGIN && col > 0) {
ab_append(&out, "\n", 1);
ab_append(&out, indent, indent_len);
col = indent_len;
}
if (col > 0) {
ab_append(&out, " ", 1);
col++;
}
ab_append(&out, word, wlen);
col += wlen;
}
if (col > 0) {
ab_append(&out, "\n", 1);
}
/* the old switcharoo */
buf = out;
ab_free(&out);
for (i = end_row; i >= start_row; i--) {
delete_row(i);
}
s = buf.b;
while ((e = strchr(s, '\n'))) {
erow_insert(start_row++, s, e - s);
s = e + 1;
}
ab_free(&buf);
EDIRTY++;
editor_set_status("Region reflowed to %d columns", REFLOW_MARGIN);
}
static inline
void clamp_curx_to_row(void)
{
abuf *row = NULL;
int maxx = 0;
if (ECURY >= ENROWS) {
return;
}
row = &EROW[ECURY];
if (ECURX < 0) {
ECURX = 0;
}
maxx = (int) row->size;
if (ECURX > maxx) {
ECURX = maxx;
}
}
static inline
void set_cursor(int col, int row)
{
if (row < 0) {
row = 0;
}
if (row > ENROWS) {
row = ENROWS;
}
ECURY = row;
ECURX = col;
clamp_curx_to_row();
}

86
term.c
View File

@@ -1,86 +0,0 @@
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "abuf.h"
#include "core.h"
#include "term.h"
#define ESCSEQ "\x1b["
static struct termios saved_entry_term;
void
enable_termraw(void)
{
struct termios raw;
if (tcgetattr(STDIN_FILENO, &raw) == -1) {
die("tcgetattr while enabling raw mode");
}
cfmakeraw(&raw);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {
die("tcsetattr while enabling raw mode");
}
}
void
display_clear(abuf *ab)
{
if (ab == NULL) {
kwrite(STDOUT_FILENO, ESCSEQ "2J", 4);
kwrite(STDOUT_FILENO, ESCSEQ "H", 3);
} else {
ab_append(ab, ESCSEQ "2J", 4);
ab_append(ab, ESCSEQ "H", 3);
}
}
void
disable_termraw(void)
{
display_clear(NULL);
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_entry_term) == -1) {
die("couldn't disable terminal raw mode");
}
}
void
setup_terminal(void)
{
if (tcgetattr(STDIN_FILENO, &saved_entry_term) == -1) {
die("can't snapshot terminal settings");
}
enable_termraw();
}
int
get_winsz(size_t *rows, size_t *cols)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
return -1;
}
*cols = (size_t)ws.ws_col;
*rows = (size_t)ws.ws_row;
return 0;
}

22
term.h
View File

@@ -1,22 +0,0 @@
#ifndef KE_TERM_H
#define KE_TERM_H
#include "abuf.h"
/* Terminal control/setup API */
void enable_termraw(void);
void disable_termraw(void);
void setup_terminal(void);
void display_clear(abuf *ab);
/*
* get_winsz uses the TIOCGWINSZ to get the window size.
*
* there's a fallback way to do this, too, that involves moving the
* cursor down and to the left \x1b[999C\x1b[999B. I'm going to skip
* on this for now because it's bloaty and this works on OpenBSD and
* Linux, at least.
*/
int get_winsz(size_t *rows, size_t *cols);
#endif /* KE_TERM_H */

111
terminal.cc Normal file
View File

@@ -0,0 +1,111 @@
#include "terminal.h"
#include <sys/ioctl.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
namespace ke {
static Terminal* g_terminal_instance = nullptr;
static void cleanup_terminal_on_exit() {
if (g_terminal_instance) {
g_terminal_instance->disable_raw_mode();
}
}
Terminal::Terminal() : raw_mode_enabled_(false) {
g_terminal_instance = this;
}
Terminal::~Terminal() {
if (raw_mode_enabled_) {
disable_raw_mode();
}
g_terminal_instance = nullptr;
}
void Terminal::setup() {
if (tcgetattr(STDIN_FILENO, &entry_term_) == -1) {
perror("can't snapshot terminal settings");
exit(1);
}
atexit(cleanup_terminal_on_exit);
enable_raw_mode();
}
void Terminal::enable_raw_mode() {
if (raw_mode_enabled_) {
return;
}
struct termios raw;
/* Read the current terminal parameters for standard input. */
if (tcgetattr(STDIN_FILENO, &raw) == -1) {
perror("tcgetattr while enabling raw mode");
exit(1);
}
/*
* Put the terminal into raw mode.
*/
cfmakeraw(&raw);
/*
* Set timeout for read(2).
*
* VMIN: what is the minimum number of bytes required for read
* to return?
*
* VTIME: max time before read(2) returns in hundreds of milli-
* seconds.
*/
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
/*
* Now write the terminal parameters to the current terminal,
* after flushing any waiting input out.
*/
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {
perror("tcsetattr while enabling raw mode");
exit(1);
}
raw_mode_enabled_ = true;
}
void Terminal::disable_raw_mode() {
if (!raw_mode_enabled_) {
return;
}
clear_screen();
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &entry_term_) == -1) {
perror("couldn't disable terminal raw mode");
exit(1);
}
raw_mode_enabled_ = false;
}
int Terminal::get_window_size(int* rows, int* cols) {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
return -1;
}
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
void Terminal::clear_screen() {
(void)write(STDOUT_FILENO, "\x1b[2J", 4);
(void)write(STDOUT_FILENO, "\x1b[H", 3);
}
} // namespace ke

39
terminal.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef TERMINAL_HPP
#define TERMINAL_HPP
#include <termios.h>
namespace ke {
/**
* Terminal management class for handling raw mode and terminal operations.
*/
class Terminal {
public:
Terminal();
~Terminal();
// Deleted copy constructor and assignment
Terminal(const Terminal&) = delete;
Terminal& operator=(const Terminal&) = delete;
// Setup and teardown
void setup();
void enable_raw_mode();
void disable_raw_mode();
// Terminal operations
static int get_window_size(int* rows, int* cols);
static void clear_screen();
// Access to original terminal settings
const termios& entry_term() const { return entry_term_; }
private:
termios entry_term_;
bool raw_mode_enabled_;
};
} // namespace ke
#endif // TERMINAL_HPP

114
undo.c
View File

@@ -1,114 +0,0 @@
#include <assert.h>
#include <stdlib.h>
#include "abuf.h"
#include "undo.h"
undo_node *
undo_node_new(undo_kind kind)
{
undo_node *node = NULL;
node = (undo_node *)malloc(sizeof(undo_node));
assert(node != NULL);
node->kind = kind;
node->row = node->col = 0;
ab_init(&node->text);
node->next = NULL;
node->parent = NULL;
return node;
}
void
undo_node_free(undo_node *node)
{
undo_node *next = NULL;
if (node == NULL) {
return;
}
ab_free(&node->text);
}
void
undo_node_free_all(undo_node *node)
{
undo_node *next = NULL;
if (node == NULL) {
return;
}
while (node != NULL) {
next = node->next;
undo_node_free(node);
free(node);
node = next;
}
}
void
undo_tree_init(undo_tree *tree)
{
assert(tree != NULL);
tree->root = NULL;
tree->current = NULL;
tree->pending = NULL;
}
void
undo_tree_free(undo_tree *tree)
{
assert(tree != NULL);
undo_node_free(tree->pending);
undo_node_free_all(tree->root);
undo_tree_init(tree);
}
void
undo_begin(undo_tree *tree, undo_kind kind)
{
undo_node *pending = NULL;
if (tree->pending != NULL) {
if (tree->pending->kind == kind) {
/* don't initiate a new undo sequence if it's the same kind */
return;
}
undo_commit(tree);
}
pending = undo_node_new(kind);
assert(pending != NULL);
tree->pending = pending;
}
void
undo_prepend(undo_tree *tree, abuf *buf)
{
}
void undo_append(undo_tree *tree, abuf *buf);
void undo_prependch(undo_tree *tree, char c);
void undo_appendch(undo_tree *tree, char c);
void undo_commit(undo_tree *tree);
void undo_apply(struct editor *editor);
void editor_undo(undo_tree *tree);
void editor_redo(undo_tree *tree);

50
undo.h
View File

@@ -1,50 +0,0 @@
#include <stddef.h>
#include "abuf.h"
#include "editor.h"
#ifndef KE_UNDO_H
#define KE_UNDO_H
typedef enum undo_kind {
UNDO_INSERT = 1 << 0,
UNDO_UNKNOWN = 1 << 1,
} undo_kind;
typedef struct undo_node {
undo_kind kind;
size_t row, col;
abuf text;
struct undo_node *next;
struct undo_node *parent;
} undo_node;
typedef struct undo_tree {
undo_node *root; /* the start of the undo sequence */
undo_node *current; /* where we are currently at */
undo_node *pending; /* the current undo operations being built */
} undo_tree;
undo_node *undo_node_new(undo_kind kind);
void undo_node_free(undo_node *node);
void undo_tree_init(undo_tree *tree);
void undo_tree_free(undo_tree *tree);
void undo_begin(undo_tree *tree, undo_kind kind);
void undo_prepend(undo_tree *tree, abuf *buf);
void undo_append(undo_tree *tree, abuf *buf);
void undo_prependch(undo_tree *tree, char c);
void undo_appendch(undo_tree *tree, char c);
void undo_commit(undo_tree *tree);
void undo_apply(struct editor *editor);
void editor_undo(undo_tree *tree);
void editor_redo(undo_tree *tree);
#endif