11 Commits

Author SHA1 Message Date
0cb7d36f2a bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-01 11:22:47 -08:00
09a6df0c33 Add regex search, search/replace, and buffer read-only mode functionality with help text 2025-12-01 02:54:40 -08:00
69457c424c add regex and search/replace functionality to editor 2025-11-30 23:33:17 -08:00
24c8040d8a trash flake-gui 2025-11-30 22:02:43 -08:00
e869249a7c thaaaaanks jeremy 2025-11-30 22:02:23 -08:00
35e957b326 Fix void crash in kge.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-11-30 21:51:03 -08:00
e7eb35626c NixOS build 2025-11-30 21:07:41 -08:00
f9128a336d better support for smaller screens
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
editor_prompt should replace the status line when active
2025-11-30 20:25:53 -08:00
f8d0e9213f bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Also add some TODOs.
2025-11-30 19:57:05 -08:00
68286ecb7c Add icon to share/icons for Linux. 2025-11-30 19:55:03 -08:00
44807d0f40 clang-tidy was zealous on macOS. 2025-11-30 19:47:43 -08:00
21 changed files with 1512 additions and 548 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
!.idea
cmake-build*
build
/imgui.ini
result

49
.idea/workspace.xml generated
View File

@@ -33,9 +33,20 @@
</configurations>
</component>
<component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Actually add the screenshot.">
<change beforePath="$PROJECT_DIR$/.github/workflows/release.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.github/workflows/release.yml" afterDir="false" />
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="add regex and search/replace functionality to editor">
<change afterPath="$PROJECT_DIR$/HelpText.cc" afterDir="false" />
<change afterPath="$PROJECT_DIR$/HelpText.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ROADMAP.md" beforeDir="false" afterPath="$PROJECT_DIR$/ROADMAP.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -62,10 +73,14 @@
<component name="HighlightingSettingsPerFile">
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
</component>
<component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="AISelfReview" />
</component>
<component name="ProjectApplicationVersion">
<option name="ide" value="CLion" />
<option name="majorVersion" value="2025" />
@@ -128,9 +143,9 @@
</key>
</component>
<component name="RunManager" selected="CMake Application.kge">
<configuration default="true" type="CLionExternalRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
<method v="2">
<option name="CLION.EXTERNAL.BUILD" enabled="true" />
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
@@ -138,7 +153,7 @@
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$PROJECT_DIR$/cmake-build-debug/test.txt" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
@@ -169,7 +184,7 @@
<workItem from="1764539556448" duration="156000" />
<workItem from="1764539725338" duration="1075000" />
<workItem from="1764542392763" duration="3512000" />
<workItem from="1764548345516" duration="9962000" />
<workItem from="1764548345516" duration="28341000" />
</task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" />
@@ -283,7 +298,23 @@
<option name="project" value="LOCAL" />
<updated>1764557759844</updated>
</task>
<option name="localTasksCounter" value="15" />
<task id="LOCAL-00015" summary="Fix void crash in kge.">
<option name="closed" value="true" />
<created>1764568264996</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1764568264996</updated>
</task>
<task id="LOCAL-00016" summary="add regex and search/replace functionality to editor">
<option name="closed" value="true" />
<created>1764574397967</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1764574397967</updated>
</task>
<option name="localTasksCounter" value="17" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -311,7 +342,9 @@
<MESSAGE value="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup." />
<MESSAGE value="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0." />
<MESSAGE value="Actually add the screenshot." />
<option name="LAST_COMMIT_MESSAGE" value="Actually add the screenshot." />
<MESSAGE value="Fix void crash in kge." />
<MESSAGE value="add regex and search/replace functionality to editor" />
<option name="LAST_COMMIT_MESSAGE" value="add regex and search/replace functionality to editor" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

View File

@@ -36,6 +36,7 @@ Buffer::Buffer(const Buffer &other)
filename_ = other.filename_;
is_file_backed_ = other.is_file_backed_;
dirty_ = other.dirty_;
read_only_ = other.read_only_;
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
@@ -60,6 +61,7 @@ Buffer::operator=(const Buffer &other)
filename_ = other.filename_;
is_file_backed_ = other.is_file_backed_;
dirty_ = other.dirty_;
read_only_ = other.read_only_;
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
@@ -82,6 +84,7 @@ Buffer::Buffer(Buffer &&other) noexcept
filename_(std::move(other.filename_)),
is_file_backed_(other.is_file_backed_),
dirty_(other.dirty_),
read_only_(other.read_only_),
mark_set_(other.mark_set_),
mark_curx_(other.mark_curx_),
mark_cury_(other.mark_cury_),
@@ -112,6 +115,7 @@ Buffer::operator=(Buffer &&other) noexcept
filename_ = std::move(other.filename_);
is_file_backed_ = other.is_file_backed_;
dirty_ = other.dirty_;
read_only_ = other.read_only_;
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
@@ -364,9 +368,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
rows_[y].insert(x, seg);
x += seg.size();
// Split line at x
std::string tail = rows_[y].substr(x);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
std::string tail = rows_[y].substr(x);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
y += 1;
x = 0;
remain.erase(0, pos + 1);
@@ -426,8 +430,8 @@ Buffer::split_line(int row, const int col)
const auto y = static_cast<std::size_t>(row);
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
const auto tail = rows_[y].substr(x);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
}
@@ -455,7 +459,7 @@ Buffer::insert_row(int row, const std::string_view text)
row = 0;
if (static_cast<std::size_t>(row) > rows_.size())
row = static_cast<int>(rows_.size());
rows_.insert(rows_.begin() + row, std::string(text));
rows_.insert(rows_.begin() + row, Line(std::string(text)));
}

View File

@@ -5,6 +5,7 @@
#define KTE_BUFFER_H
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include <string_view>
@@ -15,7 +16,7 @@
class Buffer {
public:
Buffer();
Buffer();
Buffer(const Buffer &other);
@@ -76,13 +77,13 @@ public:
Line() = default;
Line(const char *s)
explicit Line(const char *s)
{
assign_from(s ? std::string(s) : std::string());
}
Line(const std::string &s)
explicit Line(const std::string &s)
{
assign_from(s);
}
@@ -138,29 +139,38 @@ public:
// conversions
operator std::string() const
explicit operator std::string() const
{
return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size());
return {buf_.Data() ? buf_.Data() : "", buf_.Size()};
}
// string-like API used by command/renderer layers (implemented via materialization for now)
std::string substr(std::size_t pos) const
[[nodiscard]] std::string substr(std::size_t pos) const
{
const std::size_t n = buf_.Size();
if (pos >= n)
return std::string();
return std::string(buf_.Data() + pos, n - pos);
return {};
return {buf_.Data() + pos, n - pos};
}
std::string substr(std::size_t pos, std::size_t len) const
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
{
const std::size_t n = buf_.Size();
if (pos >= n)
return std::string();
return {};
const std::size_t take = (pos + len > n) ? (n - pos) : len;
return std::string(buf_.Data() + pos, take);
return {buf_.Data() + pos, take};
}
// minimal find() to support search within a line
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
{
// Materialize to std::string for now; Line is backed by AppendBuffer
const auto s = static_cast<std::string>(*this);
return s.find(needle, pos);
}
@@ -252,6 +262,14 @@ public:
return filename_;
}
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
// This does not mark the buffer as file-backed.
void SetVirtualName(const std::string &name)
{
filename_ = name;
is_file_backed_ = false;
}
[[nodiscard]] bool IsFileBacked() const
{
@@ -259,26 +277,42 @@ public:
}
[[nodiscard]] bool Dirty() const
{
return dirty_;
}
[[nodiscard]] bool Dirty() const
{
return dirty_;
}
// Read-only flag
[[nodiscard]] bool IsReadOnly() const
{
return read_only_;
}
void SetReadOnly(bool ro)
{
read_only_ = ro;
}
void ToggleReadOnly()
{
read_only_ = !read_only_;
}
void SetCursor(std::size_t x, std::size_t y)
void SetCursor(const std::size_t x, const std::size_t y)
{
curx_ = x;
cury_ = y;
}
void SetRenderX(std::size_t rx)
void SetRenderX(const std::size_t rx)
{
rx_ = rx;
}
void SetOffsets(std::size_t row, std::size_t col)
void SetOffsets(const std::size_t row, const std::size_t col)
{
rowoffs_ = row;
coloffs_ = col;
@@ -298,7 +332,7 @@ public:
}
void SetMark(std::size_t x, std::size_t y)
void SetMark(const std::size_t x, const std::size_t y)
{
mark_set_ = true;
mark_curx_ = x;
@@ -343,20 +377,21 @@ public:
// Undo system accessors (created per-buffer)
UndoSystem *Undo();
const UndoSystem *Undo() const;
[[nodiscard]] const UndoSystem *Undo() const;
private:
// State mirroring original C struct (without undo_tree)
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
std::size_t rx_ = 0; // render x (tabs expanded)
std::size_t nrows_ = 0; // number of rows
// State mirroring original C struct (without undo_tree)
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
std::size_t rx_ = 0; // render x (tabs expanded)
std::size_t nrows_ = 0; // number of rows
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
std::vector<Line> rows_; // buffer rows (without trailing newlines)
std::string filename_;
bool is_file_backed_ = false;
bool dirty_ = false;
bool mark_set_ = false;
std::size_t mark_curx_ = 0, mark_cury_ = 0;
bool is_file_backed_ = false;
bool dirty_ = false;
bool read_only_ = false;
bool mark_set_ = false;
std::size_t mark_curx_ = 0, mark_cury_ = 0;
// Per-buffer undo state
std::unique_ptr<struct UndoTree> undo_tree_;

View File

@@ -4,14 +4,15 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.0.1")
set(KTE_VERSION "1.0.5")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
set(BUILD_GUI OFF CACHE BOOL "Enable building the graphical version.")
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
if (CMAKE_HOST_UNIX)
message(STATUS "Build system is POSIX.")
@@ -55,6 +56,7 @@ set(COMMON_SOURCES
Buffer.cc
Editor.cc
Command.cc
HelpText.cc
KKeymap.cc
TerminalInputHandler.cc
TerminalRenderer.cc
@@ -74,6 +76,7 @@ set(COMMON_HEADERS
Editor.h
AppendBuffer.h
Command.h
HelpText.h
KKeymap.h
InputHandler.h
TerminalInputHandler.h
@@ -99,6 +102,9 @@ add_executable(kte
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kte ${CURSES_LIBRARIES})
@@ -121,6 +127,10 @@ if (BUILD_TESTS)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(test_undo ${CURSES_LIBRARIES})
endif ()
@@ -153,6 +163,9 @@ if (${BUILD_GUI})
GUIFrontend.cc
GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
# On macOS, build kge as a proper .app bundle
@@ -201,4 +214,5 @@ if (${BUILD_GUI})
endif ()
# Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif ()

File diff suppressed because it is too large Load Diff

View File

@@ -23,8 +23,10 @@ enum class CommandId {
Refresh, // force redraw
KPrefix, // show "C-k _" prompt in status when entering k-command
FindStart, // begin incremental search (placeholder)
RegexFindStart, // begin regex search (C-r)
RegexpReplace, // begin regex search & replace (C-t)
SearchReplace, // begin search & replace (two-step prompt)
OpenFileStart, // begin open-file prompt
// GUI: visual file picker
VisualFilePickerToggle,
// Buffers
BufferSwitchStart, // begin buffer switch prompt
@@ -71,6 +73,8 @@ enum class CommandId {
IndentRegion, // indent region (C-k =)
UnindentRegion, // unindent region (C-k -)
ReflowParagraph, // reflow paragraph to column width (ESC q)
// Read-only buffers
ToggleReadOnly, // toggle current buffer read-only (C-k ')
// Buffer operations
ReloadBuffer, // reload buffer from disk (C-k l)
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
@@ -78,6 +82,8 @@ enum class CommandId {
JumpToLine, // prompt for line and jump (C-k g)
ShowWorkingDirectory, // Display the current working directory in the editor message.
ChangeWorkingDirectory, // Change the editor's current directory.
// Help
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
// Meta
UnknownKCommand, // arg: single character that was not recognized after C-k
};

View File

@@ -301,8 +301,22 @@ public:
}
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
enum class PromptKind {
None = 0,
Search,
RegexSearch,
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
OpenFile,
SaveAs,
Confirm,
BufferSwitch,
GotoLine,
Chdir,
ReplaceFind, // step 1 of Search & Replace: find what
ReplaceWith // step 2 of Search & Replace: replace with
};
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
@@ -504,9 +518,20 @@ private:
std::string prompt_text_;
std::string pending_overwrite_path_;
// GUI-only state (safe no-op in terminal builds)
bool file_picker_visible_ = false;
std::string file_picker_dir_;
// GUI-only state (safe no-op in terminal builds)
bool file_picker_visible_ = false;
std::string file_picker_dir_;
// Temporary state for Search & Replace flow
public:
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
private:
std::string replace_find_tmp_;
std::string replace_with_tmp_;
};
#endif // KTE_EDITOR_H

View File

@@ -1,10 +1,13 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <limits>
#include <string>
#include <imgui.h>
#include <regex>
#include "GUIRenderer.h"
#include "Buffer.h"
@@ -149,14 +152,14 @@ GUIRenderer::Draw(Editor &ed)
}
}
}
// Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;
// Compute viewport-relative row so (0) is top row of the visible area
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
long vy = static_cast<long>(vy_f);
if (vy < 0)
vy = 0;
// Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;
// Compute viewport-relative row so (0) is top row of the visible area
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
long vy = static_cast<long>(vy_f);
if (vy < 0)
vy = 0;
// Clamp vy within visible content height to avoid huge jumps
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
@@ -168,97 +171,163 @@ GUIRenderer::Draw(Editor &ed)
if (vy >= vis_rows)
vy = vis_rows - 1;
// Translate viewport row to buffer row using Buffer::Rowoffs
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
if (by >= lines.size()) {
if (!lines.empty())
by = lines.size() - 1;
else
by = 0;
}
// Translate viewport row to buffer row using Buffer::Rowoffs
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
if (by >= lines.size()) {
if (!lines.empty())
by = lines.size() - 1;
else
by = 0;
}
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
float px = (mp.x - list_origin.x - scroll_x);
if (px < 0.0f)
px = 0.0f;
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
float px = (mp.x - list_origin.x - scroll_x);
if (px < 0.0f)
px = 0.0f;
// Convert pixel X to a render-column target including horizontal col offset
// Use our own tab expansion of width 8 to match command layer logic.
const std::string &line_clicked = lines[by];
const std::size_t tabw = 8;
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
// then translate to viewport-space by subtracting Coloffs.
std::size_t coloffs = buf->Coloffs();
std::size_t rx_abs = 0; // absolute rendered column
std::size_t i = 0; // source column iterator
// Empty buffer guard: if there are no lines yet, just move to 0:0
if (lines.empty()) {
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
} else {
// Convert pixel X to a render-column target including horizontal col offset
// Use our own tab expansion of width 8 to match command layer logic.
std::string line_clicked = static_cast<std::string>(lines[by]);
const std::size_t tabw = 8;
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
// then translate to viewport-space by subtracting Coloffs.
std::size_t coloffs = buf->Coloffs();
std::size_t rx_abs = 0; // absolute rendered column
std::size_t i = 0; // source column iterator
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
if (!line_clicked.empty() && coloffs > 0) {
while (i < line_clicked.size() && rx_abs < coloffs) {
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
}
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
if (!line_clicked.empty() && coloffs > 0) {
while (i < line_clicked.size() && rx_abs < coloffs) {
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
}
// Now search for closest source column to clicked px within/after viewport
std::size_t best_col = i; // default to first visible column
float best_dist = std::numeric_limits<float>::infinity();
while (true) {
// For i in [current..size], evaluate candidate including the implicit end position
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
float rx_px = static_cast<float>(rx_view) * space_w;
float dist = std::fabs(px - rx_px);
if (dist <= best_dist) {
best_dist = dist;
best_col = i;
}
if (i == line_clicked.size())
break;
// advance to next source column
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
// Now search for closest source column to clicked px within/after viewport
std::size_t best_col = i; // default to first visible column
float best_dist = std::numeric_limits<float>::infinity();
while (true) {
// For i in [current..size], evaluate candidate including the implicit end position
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
float rx_px = static_cast<float>(rx_view) * space_w;
float dist = std::fabs(px - rx_px);
if (dist <= best_dist) {
best_dist = dist;
best_col = i;
}
if (i == line_clicked.size())
break;
// advance to next source column
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
// Dispatch absolute buffer coordinates (row:col)
char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
}
// Dispatch absolute buffer coordinates (row:col)
char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
}
}
// Cache current horizontal offset in rendered columns
const std::size_t coloffs_now = buf->Coloffs();
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
// Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos();
const std::string &line = lines[i];
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
// Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos();
std::string line = static_cast<std::string>(lines[i]);
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
const std::size_t tabw = 8;
std::string expanded;
expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; // rendered column for drawing
// Emit entire line (ImGui child scrolling will handle clipping)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
// Emit spaces for the tab
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
const std::size_t tabw = 8;
std::string expanded;
expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; // rendered column for drawing
// Compute search highlight ranges for this line in source indices
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
if (search_mode) {
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
std::size_t sx = static_cast<std::size_t>(m.position());
std::size_t ex = sx + static_cast<std::size_t>(m.length());
hl_src_ranges.emplace_back(sx, ex);
}
} catch (const std::regex_error &) {
// ignore invalid patterns here; status line already shows the error
}
} else {
const std::string &q = ed.SearchQuery();
std::size_t pos = 0;
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
hl_src_ranges.emplace_back(pos, pos + q.size());
pos += q.size();
}
}
}
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
std::size_t rx = 0;
std::size_t s = 0;
while (s < upto_src_exclusive && s < line.size()) {
if (line[s] == '\t')
rx += (tabw - (rx % tabw));
else
rx += 1;
++s;
}
return rx;
};
// Draw background highlights (under text)
if (search_mode && !hl_src_ranges.empty()) {
// Current match emphasis
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
for (const auto &rg : hl_src_ranges) {
std::size_t sx = rg.first, ex = rg.second;
std::size_t rx_start = src_to_rx(sx);
std::size_t rx_end = src_to_rx(ex);
// Apply horizontal scroll offset
if (rx_end <= coloffs_now) continue; // fully left of view
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w, line_pos.y + line_h);
// Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end;
ImU32 col = is_current ? IM_COL32(255, 220, 120, 140) : IM_COL32(200, 200, 0, 90);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
// Emit entire line (ImGui child scrolling will handle clipping)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
// Emit spaces for the tab
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
ImGui::TextUnformatted(expanded.c_str());
ImGui::TextUnformatted(expanded.c_str());
// Draw a visible cursor indicator on the current line
if (i == cy) {
@@ -280,28 +349,106 @@ GUIRenderer::Draw(Editor &ed)
}
ImGui::EndChild();
// Status bar spanning full width
ImGui::Separator();
// Status bar spanning full width
ImGui::Separator();
// Build three segments: left (app/version/buffer/dirty), middle (message), right (cursor/mark)
// Compute full content width and draw a filled background rectangle
ImVec2 win_pos = ImGui::GetWindowPos();
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
float x0 = win_pos.x + cr_min.x;
float x1 = win_pos.x + cr_max.x;
ImVec2 cursor = ImGui::GetCursorScreenPos();
float bar_h = ImGui::GetFrameHeight();
ImVec2 p0(x0, cursor.y);
ImVec2 p1(x1, cursor.y + bar_h);
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
// Build left text
std::string left;
left.reserve(256);
left += "kge"; // GUI app name
left += " ";
left += KTE_VERSION_STR;
// Compute full content width and draw a filled background rectangle
ImVec2 win_pos = ImGui::GetWindowPos();
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
float x0 = win_pos.x + cr_min.x;
float x1 = win_pos.x + cr_max.x;
ImVec2 cursor = ImGui::GetCursorScreenPos();
float bar_h = ImGui::GetFrameHeight();
ImVec2 p0(x0, cursor.y);
ImVec2 p1(x1, cursor.y + bar_h);
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
// If a prompt is active, replace the entire status bar with the prompt text
if (ed.PromptActive()) {
std::string label = ed.PromptLabel();
std::string ptext = ed.PromptText();
auto kind = ed.CurrentPromptKind();
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
const char *home_c = std::getenv("HOME");
if (home_c && *home_c) {
std::string home(home_c);
if (ptext.rfind(home, 0) == 0) {
std::string rest = ptext.substr(home.size());
if (rest.empty())
ptext = "~";
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
ptext = std::string("~") + rest;
}
}
}
float pad = 6.f;
float left_x = p0.x + pad;
float right_x = p1.x - pad;
float max_px = std::max(0.0f, right_x - left_x);
std::string prefix;
if (!label.empty()) prefix = label + ": ";
// Compose showing right-end of filename portion when too long for space
std::string final_msg;
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && avail_px > 0.0f) {
// Trim from left until it fits by pixel width
std::string tail = ptext;
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
if (tail_sz.x > avail_px) {
// Remove leading chars until it fits
// Use a simple loop; text lengths are small here
size_t start = 0;
// To avoid O(n^2) worst-case, remove chunks
while (start < tail.size()) {
// Estimate how many chars to skip based on ratio
float ratio = tail_sz.x / avail_px;
size_t skip = ratio > 1.5f ? std::min(tail.size() - start, (size_t)std::max<size_t>(1, (size_t)(tail.size() / 4))) : 1;
start += skip;
std::string candidate = tail.substr(start);
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
if (cand_sz.x <= avail_px) {
tail = candidate;
tail_sz = cand_sz;
break;
}
}
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
// As a last resort, ensure fit by chopping exactly
// binary reduce
size_t lo = 0, hi = tail.size();
while (lo < hi) {
size_t mid = (lo + hi) / 2;
std::string cand = tail.substr(mid);
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1;
}
tail = tail.substr(lo);
}
}
final_msg = prefix + tail;
} else {
final_msg = prefix + ptext;
}
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
ImGui::TextUnformatted(final_msg.c_str());
ImGui::PopClipRect();
// Advance cursor to after the bar to keep layout consistent
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
} else {
// Build left text
std::string left;
left.reserve(256);
left += "kge"; // GUI app name
left += " ";
left += KTE_VERSION_STR;
std::string fname;
try {
fname = ed.DisplayNameFor(*buf);
@@ -400,8 +547,9 @@ GUIRenderer::Draw(Editor &ed)
ImGui::PopClipRect();
}
}
// Advance cursor to after the bar to keep layout consistent
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
// Advance cursor to after the bar to keep layout consistent
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
}
}
ImGui::End();

55
HelpText.cc Normal file
View File

@@ -0,0 +1,55 @@
/*
* HelpText.cc - embedded/customizable help content
*/
#include "HelpText.h"
std::string
HelpText::Text()
{
// Customize the help text here. This string will be used by C-k h first.
// You can keep it empty to fall back to the manpage or built-in defaults.
// Note: keep newline characters as-is; the renderer splits lines on '\n'.
return std::string(
"KTE - Kyle's Text Editor\n\n"
"About:\n"
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n"
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n"
" process of writing a text editor from scratch. It has keybindings inspired by\n"
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
"\n"
"Core keybindings:\n"
" C-k ' Toggle read-only\n"
" C-k - Unindent region\n"
" C-k = Indent region\n"
" C-k C-d Kill entire line\n"
" C-k C-q Quit now (no confirm)\n"
" C-k a Mark all and jump to end\n"
" C-k b Switch buffer\n"
" C-k c Close current buffer\n"
" C-k d Kill to end of line\n"
" C-k e Open file (prompt)\n"
" C-k g Jump to line\n"
" C-k h Show this help\n"
" C-k l Reload buffer from disk\n"
" C-k n Previous buffer\n"
" C-k o Change working directory (prompt)\n"
" C-k p Next buffer\n"
" C-k q Quit (confirm if dirty)\n"
" C-k r Redo\n"
" C-k s Save buffer\n"
" C-k u Undo\n"
" C-k v Toggle visual file picker (GUI)\n"
" C-k w Show working directory\n"
" C-k x Save and quit\n"
"\n"
"ESC/Alt commands:\n"
" ESC q Reflow paragraph\n"
" ESC BACKSPACE Delete previous word\n"
" ESC d Delete next word\n"
" Alt-w Copy region to kill ring\n\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n"
);
}

17
HelpText.h Normal file
View File

@@ -0,0 +1,17 @@
/*
* HelpText.h - embedded/customizable help content
*/
#ifndef KTE_HELPTEXT_H
#define KTE_HELPTEXT_H
#include <string>
class HelpText {
public:
// Returns the embedded help text as a single string with newlines.
// Project maintainers can customize the returned string below
// (in HelpText.cc) without touching the help command logic.
static std::string Text();
};
#endif // KTE_HELPTEXT_H

View File

@@ -33,6 +33,10 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
out = CommandId::Redo; // C-k r (redo)
return true;
}
if (ascii_key == '\'') {
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
return true;
}
switch (k_lower) {
case 'a':
@@ -59,6 +63,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'g':
out = CommandId::JumpToLine;
return true;
case 'h':
out = CommandId::ShowHelp;
return true;
case 'j':
out = CommandId::JumpToMark;
return true;
@@ -114,7 +121,7 @@ auto
KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
{
const int k = KLowerAscii(ascii_key);
switch (k) {
switch (k) {
case 'w':
out = CommandId::KillRegion; // C-w
return true;
@@ -145,6 +152,15 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
case 's':
out = CommandId::FindStart;
return true;
case 'r':
out = CommandId::RegexFindStart; // C-r regex search
return true;
case 't':
out = CommandId::RegexpReplace; // C-t regex search & replace
return true;
case 'h':
out = CommandId::SearchReplace; // C-h: search & replace
return true;
case 'l':
out = CommandId::Refresh;
return true;

View File

@@ -1,7 +1,10 @@
ROADMAP / TODO:
- [ ] Search + Replace
- [ ] Regex search + replace
- [x] Search + Replace
- [x] Regex search + replace
- [ ] The undo system should actually work
- [ ] Able to mark buffers as read-only
- [ ] Built-in help text
- [x] Able to mark buffers as read-only
- [x] Built-in help text
- [x] Shorten paths in the homedir with ~
- [x] When the filename is longer than the message window, scoot left to
keep it in view

View File

@@ -1,7 +1,9 @@
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <cstdlib>
#include <ncurses.h>
#include <regex>
#include <string>
#include "TerminalRenderer.h"
@@ -39,100 +41,140 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t coloffs = buf->Coloffs();
const int tabw = 8;
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0;
std::size_t mx = do_hl ? ed.SearchMatchX() : 0;
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0;
bool hl_on = false;
int written = 0;
if (li < lines.size()) {
const std::string &line = lines[li];
src_i = 0;
render_col = 0;
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (src_i < line.size()) {
unsigned char c = static_cast<unsigned char>(line[src_i]);
if (c == '\t') {
std::size_t next_tab = tabw - (render_col % tabw);
if (render_col + next_tab <= coloffs) {
render_col += next_tab;
++src_i;
continue;
}
// Emit spaces for tab
if (render_col < coloffs) {
// skip to coloffs
std::size_t to_skip = std::min<std::size_t>(
next_tab, coloffs - render_col);
render_col += to_skip;
next_tab -= to_skip;
}
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = do_hl && src_i >= mx && src_i < mx + mlen;
// highlight by source index
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
addch(' ');
++written;
++render_col;
--next_tab;
}
++src_i;
continue;
} else {
// normal char
if (render_col < coloffs) {
++render_col;
++src_i;
continue;
}
ch = static_cast<char>(c);
from_src = true;
}
} else {
// beyond EOL, fill spaces
ch = ' ';
from_src = false;
}
if (do_hl) {
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
}
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++src_i;
if (src_i >= line.size() && written >= cols)
break;
}
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
clrtoeol();
}
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
// Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
if (search_mode && li < lines.size()) {
std::string sline = static_cast<std::string>(lines[li]);
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
std::size_t sx = static_cast<std::size_t>(m.position());
std::size_t ex = sx + static_cast<std::size_t>(m.length());
ranges.emplace_back(sx, ex);
}
} catch (const std::regex_error &) {
// ignore invalid patterns here; status shows error
}
} else {
const std::string &q = ed.SearchQuery();
std::size_t pos = 0;
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
ranges.emplace_back(pos, pos + q.size());
pos += q.size();
}
}
}
auto is_src_in_hl = [&](std::size_t si) -> bool {
if (ranges.empty()) return false;
// ranges are non-overlapping and ordered by construction
// linear scan is fine for now
for (const auto &rg : ranges) {
if (si < rg.first) break;
if (si >= rg.first && si < rg.second) return true;
}
return false;
};
// Track current-match to optionally emphasize
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
bool hl_on = false;
bool cur_on = false;
int written = 0;
if (li < lines.size()) {
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (src_i < line.size()) {
unsigned char c = static_cast<unsigned char>(line[src_i]);
if (c == '\t') {
std::size_t next_tab = tabw - (render_col % tabw);
if (render_col + next_tab <= coloffs) {
render_col += next_tab;
++src_i;
continue;
}
// Emit spaces for tab
if (render_col < coloffs) {
// skip to coloffs
std::size_t to_skip = std::min<std::size_t>(
next_tab, coloffs - render_col);
render_col += to_skip;
next_tab -= to_skip;
}
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && src_i >= cur_mx && src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl) attr |= A_STANDOUT;
if (in_cur) attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; }
addch(' ');
++written;
++render_col;
--next_tab;
}
++src_i;
continue;
} else {
// normal char
if (render_col < coloffs) {
++render_col;
++src_i;
continue;
}
ch = static_cast<char>(c);
from_src = true;
}
} else {
// beyond EOL, fill spaces
ch = ' ';
from_src = false;
}
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < cur_mend;
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; }
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++src_i;
if (src_i >= line.size() && written >= cols)
break;
}
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
clrtoeol();
}
// Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury();
@@ -149,13 +191,71 @@ TerminalRenderer::Draw(Editor &ed)
mvaddstr(0, 0, "[no buffer]");
}
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
move(rows - 1, 0);
attron(A_REVERSE);
// Status line (inverse)
move(rows - 1, 0);
attron(A_REVERSE);
// Fill the status line with spaces first
for (int i = 0; i < cols; ++i)
addch(' ');
// Fill the status line with spaces first
for (int i = 0; i < cols; ++i)
addch(' ');
// If a prompt is active, replace the status bar with the full prompt text
if (ed.PromptActive()) {
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
std::string label = ed.PromptLabel();
std::string ptext = ed.PromptText();
auto kind = ed.CurrentPromptKind();
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
const char *home_c = std::getenv("HOME");
if (home_c && *home_c) {
std::string home(home_c);
// Ensure we match only at the start
if (ptext.rfind(home, 0) == 0) {
std::string rest = ptext.substr(home.size());
if (rest.empty())
ptext = "~";
else if (rest[0] == '/' || rest[0] == '\\')
ptext = std::string("~") + rest;
}
}
}
// Prefer keeping the tail of the filename visible when it exceeds the window
std::string msg;
if (!label.empty()) {
msg = label + ": ";
}
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && cols > 0) {
int avail = cols - static_cast<int>(msg.size());
if (avail <= 0) {
// No room for label; fall back to showing the rightmost portion of the whole string
std::string whole = msg + ptext;
if ((int)whole.size() > cols)
whole = whole.substr(whole.size() - cols);
msg = whole;
} else {
if ((int)ptext.size() > avail) {
ptext = ptext.substr(ptext.size() - avail);
}
msg += ptext;
}
} else {
// Non-file prompts: simple concatenation and clip by terminal
msg += ptext;
}
// Draw left-aligned, clipped to width
if (!msg.empty())
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
// End status rendering for prompt mode
attroff(A_REVERSE);
// Restore logical cursor position in content area
if (saved_cur_y >= 0 && saved_cur_x >= 0)
move(saved_cur_y, saved_cur_x);
return;
}
// Build left segment
std::string left;
@@ -195,6 +295,9 @@ TerminalRenderer::Draw(Editor &ed)
left += fname;
if (b && b->Dirty())
left += " *";
// Append read-only indicator
if (b && b->IsReadOnly())
left += " [RO]";
// Append total line count as "<n>L"
if (b) {
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
@@ -243,10 +346,10 @@ TerminalRenderer::Draw(Editor &ed)
if (llen > 0)
mvaddnstr(rows - 1, 0, left.c_str(), llen);
// Draw right, flush to end
int rstart = std::max(0, cols - rlen);
if (rlen > 0)
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
// Draw right, flush to end
int rstart = std::max(0, cols - rlen);
if (rlen > 0)
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
// Middle message
const std::string &msg = ed.Status();
@@ -262,7 +365,7 @@ TerminalRenderer::Draw(Editor &ed)
}
}
attroff(A_REVERSE);
attroff(A_REVERSE);
// Restore terminal cursor to the content position so a visible caret
// remains in the editing area (not on the status line).

View File

@@ -1,10 +1,11 @@
#ifndef KTE_UNDONODE_H
#define KTE_UNDONODE_H
#include <cstdint>
#include <string>
enum class UndoType : uint8_t {
enum class UndoType : std::uint8_t {
Insert,
Delete,
Paste,

View File

@@ -1,51 +0,0 @@
{
lib,
stdenv,
cmake,
ncurses,
SDL2,
libGL,
xorg,
installShellFiles,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "kte";
inherit version;
src = lib.cleanSource ./.;
nativeBuildInputs = [
cmake
ncurses
SDL2
libGL
xorg.libX11
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp kte $out/bin/
cp kge $out/bin/
installManPage ../docs/kte.1
installManPage ../docs/kge.1
runHook postInstall
'';
}

View File

@@ -1,42 +0,0 @@
{
lib,
stdenv,
cmake,
ncurses,
installShellFiles,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "kte";
inherit version;
src = lib.cleanSource ./.;
nativeBuildInputs = [
cmake
ncurses
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=OFF"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp kte $out/bin/
installManPage ../docs/kte.1
runHook postInstall
'';
}

View File

@@ -7,12 +7,16 @@
libGL,
xorg,
installShellFiles,
graphical ? false,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
versionLine = lib.findFirst (
l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null
) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
@@ -24,14 +28,16 @@ stdenv.mkDerivation {
nativeBuildInputs = [
cmake
ncurses
installShellFiles
]
++ lib.optionals graphical [
SDL2
libGL
xorg.libX11
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
"-DCMAKE_BUILD_TYPE=Debug"
];
@@ -40,11 +46,17 @@ stdenv.mkDerivation {
mkdir -p $out/bin
cp kte $out/bin/
cp kge $out/bin/
installManPage ../docs/kte.1
installManPage ../docs/kge.1
''
+ lib.optionalString graphical ''
cp kge $out/bin/
installManPage ../docs/kge.1
mkdir -p $out/share/icons
cp ../kge.png $out/share/icons/
''
+ ''
runHook postInstall
'';
}

View File

@@ -1,55 +0,0 @@
# flake.nix
{
description = "kte ImGui/SDL2 text editor";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.default = pkgs.stdenv.mkDerivation {
pname = "kte";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
buildInputs = with pkgs; [
ncurses
SDL2
libGL
xorg.libX11
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCURSES_NEED_NCURSES=TRUE"
"-DCURSES_NEED_WIDE=TRUE"
];
# Alternative (even stronger): completely hide the broken module
preConfigure = ''
# If the project ships its own FindSDL2.cmake in cmake/, hide it
if [ -f cmake/FindSDL2.cmake ]; then
mv cmake/FindSDL2.cmake cmake/FindSDL2.cmake.disabled
echo "Disabled bundled FindSDL2.cmake"
fi
'';
meta = with pkgs.lib; {
description = "kte ImGui/SDL2 GUI editor";
mainProgram = "kte";
platforms = platforms.linux;
};
};
devShells.default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.default ];
packages = with pkgs; [ gdb clang-tools ];
};
});
}

10
flake.lock generated
View File

@@ -2,15 +2,15 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1764242076,
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
"owner": "nixos",
"lastModified": 1764517877,
"narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
"rev": "2d293cbfa5a793b4c50d17c05ef9e385b90edf6c",
"type": "github"
},
"original": {
"owner": "nixos",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"

View File

@@ -1,21 +1,20 @@
{
description = "Kyle's Text Editor";
description = "kyle's text editor";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
inputs@{ self, nixpkgs, ... }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
pkgsFor = system: import nixpkgs { inherit system; };
in
{
packages.x86_64-linux = {
default = pkgs.callPackage ./default-nogui.nix { };
kge = pkgs.callPackage ./default-gui.nix { };
kte = pkgs.callPackage ./default-nogui.nix { };
full = pkgs.callPackage ./default.nix { };
};
packages = eachSystem (system: rec {
default = kte;
full = kge;
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
});
};
}