55 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
7245003769 cleanups 2025-11-24 23:05:40 -08:00
542b1d90a0 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 22:59:40 -08:00
a359f4e6c4 fix Makefile CFLAGS for nix 2025-11-24 22:59:09 -08:00
a03dd0c433 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 22:52:25 -08:00
561faf537c Updating docs. 2025-11-24 22:51:10 -08:00
3a36b35c1f support running cloc over the file 2025-11-24 22:51:10 -08:00
Jeremy O'Brien
b1cb2532f6 add flake + default.nix 2025-11-24 22:43:40 -08:00
9f33cdc0e4 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 22:15:11 -08:00
afcc33329f add C-k l to get lines of code 2025-11-24 22:14:51 -08:00
128235283c bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
1.3.0: mark/region
2025-11-24 21:41:04 -08:00
af3c0eddc4 set editor position 2025-11-24 21:38:04 -08:00
f8f01f7dd6 don't clear mark in kill/delete_region 2025-11-24 21:32:23 -08:00
8206ab0a40 fixed counting, I think 2025-11-24 21:21:37 -08:00
af8dcb847e working on delete-region 2025-11-24 21:14:56 -08:00
9434a34116 kill_region appears to work 2025-11-24 20:49:55 -08:00
3b2b60466c continuing region work 2025-11-24 20:27:30 -08:00
52e1f2dd8b Cleaning up; start mark/region work. 2025-11-24 20:07:15 -08:00
8bb9228339 clean asan logs 2025-11-24 19:17:06 -08:00
e937e3398c Dumb typo.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 14:41:11 -08:00
f147c866ab v1.2.0 with basic killring.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 14:37:33 -08:00
1227d0abf4 Support basic killing/yanking. 2025-11-24 14:35:44 -08:00
d3591331a5 Update man page with yank. 2025-11-24 14:03:12 -08:00
5eafc1a34b local install in Makefile 2025-11-24 13:57:10 -08:00
a0103dd5aa Start killring work. 2025-11-24 13:46:00 -08:00
3782880062 UTF-8 support. 2025-11-24 13:11:24 -08:00
e345b55595 Fix version number
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 11:05:51 -08:00
d1978a3b98 Code cleanups and editor fixups.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-24 10:54:48 -08:00
5ee8234ab7 Update the manpage.
Print the core dump k-command correctly.
2025-11-24 10:23:39 -08:00
f82d1f8831 move nix to nixos config 2025-11-24 10:22:48 -08:00
2019ec10ce local testing updates 2025-11-23 19:15:19 -08:00
0110f82705 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-23 14:26:17 -08:00
a62a8e50fa find_prev_word and delete_prev_word work. 2025-11-23 14:24:49 -08:00
33e19e7d76 add Makefile for basic build 2025-11-23 11:48:46 -08:00
fd01e3593f delete next word complete, delete prev in progress. 2025-11-23 11:48:35 -08:00
47bbc5339c forward/backward word. 2025-11-23 00:19:25 -08:00
2962a6c92e start word nav 2025-11-22 23:31:49 -08:00
7fa887273b move nix to nixos config 2025-11-22 23:31:49 -08:00
fb02f37512 make ASan a compile time option 2025-11-22 23:27:34 -08:00
1722dbee0b Fix check for render realloc. 2025-11-22 12:50:22 -08:00
14199afeb5 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 12:36:41 -08:00
9f3558c430 put it in build, not build/bin
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 12:27:02 -08:00
ce64e4637d clean up 2025-11-22 12:15:53 -08:00
36013e42e4 version bump
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 12:13:56 -08:00
dd667c1ef5 revert back to older version with some previous fixes
the ai was getting wild
2025-11-22 12:13:03 -08:00
c9977b0fc0 checkpoint
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 12:00:50 -08:00
dd2c888766 update man page
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 01:48:31 -08:00
2967998893 Update README
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 01:45:59 -08:00
a400cdf5ad update README.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 01:44:41 -08:00
c8a43fb328 typo in release 2025-11-22 01:41:14 -08:00
3ea7c31cba update workflow 2025-11-22 01:39:45 -08:00
115091e517 add deployment workflow
Some checks failed
Release / GoReleaser (push) Has been cancelled
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-22 01:32:58 -08:00
1b9b618e3a backout 2025-11-22 01:27:44 -08:00
527759de09 broke it 2025-11-22 01:18:04 -08:00
29 changed files with 4562 additions and 1576 deletions

37
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch: {}
permissions:
contents: write
jobs:
homebrew:
name: Bump Homebrew formula
# Skip this job in case of git pushes to prerelease tags
if: ${{ github.event_name != 'push' || !contains(github.ref, '-') }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Extract version
id: extract-version
run: |
echo "tag-name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- uses: mislav/bump-homebrew-formula-action@v3
with:
formula-name: ke
formula-path: Formula/ke.rb
homebrew-tap: kisom/homebrew-tap
base-branch: master
commit-message: |
{{formulaName}} {{version}}
Created by https://github.com/mislav/bump-homebrew-formula-action
env:
COMMITTER_TOKEN: ${{ secrets.GH_CPAT }}

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.log
build
ke
*.txt

View File

@@ -1,21 +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 "1.0.0")
# 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)
# Source files
set(KE_SOURCES
main.cc
abuf.cc
erow.cc
terminal.cc
input_handler.cc
display.cc
file_io.cc
killring.cc
)
# Add executable
add_executable(ke main.c)
# Header files
set(KE_HEADERS
ke_constants.h
abuf.h
erow.h
terminal.h
input_handler.h
display.h
file_io.h
killring.h
)
# Define KE_VERSION for use in C code (e.g., #define KE_VERSION)
target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}")
# Build options
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
# 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")
target_compile_options(ke PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(ke PRIVATE -fsanitize=address)
endif()
# Installation rules
include(GNUInstallDirs)
install(TARGETS ke RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ke.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
# Set output properties
set_target_properties(ke PROPERTIES
FOLDER bin
RUNTIME_OUTPUT_DIRECTORY bin
)

65
Makefile Normal file
View File

@@ -0,0 +1,65 @@
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
$(TARGET): $(OBJECTS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJECTS) $(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)
install:
cp $(TARGET) $(DEST)
clean:
rm -f $(TARGET)
rm -f $(OBJECTS)
rm -f asan.log*
.PHONY: test.txt
test.txt:
cp test.txt.bak $@

View File

@@ -6,4 +6,13 @@ used fairly often.
See the man page for more info.
Released under an ISC license.
It should be available via homebrew, even:
brew tap kisom/homebrew-tap
brew install ke
To get verbose ASAN messages:
export LSAN_OPTIONS=verbosity=1:log_threads=1
Released under an ISC license.

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

57
abuf.h Normal file
View File

@@ -0,0 +1,57 @@
#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.
*/
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); }
private:
std::string buffer_;
};
} // namespace ke
#endif // ABUF_HPP

27
default.nix Normal file
View File

@@ -0,0 +1,27 @@
{
lib,
installShellFiles,
stdenv,
...
}:
stdenv.mkDerivation {
pname = "ke";
version = "1.3.3";
src = lib.cleanSource ./.;
nativeBuildInputs = [ installShellFiles ];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp ke $out/bin/
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

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

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1763835633,
"narHash": "sha256-HzxeGVID5MChuCPESuC0dlQL1/scDKu+MmzoVBJxulM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "050e09e091117c3d7328c7b2b7b577492c43c134",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

18
flake.nix Normal file
View File

@@ -0,0 +1,18 @@
{
description = "Kyle's Text Editor";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs =
{ self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux = {
default = pkgs.callPackage ./default.nix { };
};
};
}

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

39
ke.1
View File

@@ -19,21 +19,26 @@ is
K-command mode is entered using C-k. This is taken from Wordstar and just
so happens to be blessed with starting with a most excellent letter of
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.
.Pp
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
.Bl -tag -width xxxxxxxxxxxx -offset indent
.It C-k BACKSPACE
Delete from the cursor to the beginning of the line.
.It C-k C-d
Delete the current row.
.It C-k SPACE
Toggle the mark.
.It C-k d
Delete from the cursor to the end of the line.
.It C-k C-d
Delete the entire line.
.It C-k e
Edit a new file. Also C-k C-e.
.It C-k f
Incremental find.
.It C-k g
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 q
@@ -42,9 +47,33 @@ exit the editor. Also C-k C-q.
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 \
.It C-k y
Yank the killring.
.It C-k \[char92]
Dump core.
.El
.Sh OTHER KEYBINDINGS
.Bl -tag -width xxxxxxxxxxxx -offset indent
.It C-l
Refresh the display.
.It C-s
Incremental find.
.It C-w
Kill the region if the mark is set.
.It C-y
Yank the killring.
.It ESC BACKSPACE
Delete the previous word.
.It ESC b
Move to the previous word.
.It ESC d
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 killring.
.It
.El
.Sh FIND
The find operation is an incremental search. The up or left arrow keys will
go to the previous result, while the down or right arrow keys will go to

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

1556
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

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

15
test.txt.bak Normal file
View File

@@ -0,0 +1,15 @@
hello
world it is me
but I am not me