diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index b224eff..bc141fb 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -25,7 +25,6 @@
-
@@ -34,26 +33,17 @@
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
+
@@ -147,7 +137,7 @@
-
+
@@ -168,16 +158,10 @@
-
-
-
-
-
-
@@ -187,7 +171,7 @@
1764457173148
-
+
@@ -229,7 +213,15 @@
1764496151303
-
+
+
+ 1764500200942
+
+
+
+ 1764500200942
+
+
@@ -248,7 +240,8 @@
-
+
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 40d7c90..e7194b9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,11 +4,12 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17)
-set(KTE_VERSION "0.0.1")
+set(KTE_VERSION "0.1.0")
# 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_TESTS OFF CACHE BOOL "Enable building test programs.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" OFF)
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
@@ -103,19 +104,22 @@ install(TARGETS kte
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
-# test_undo executable for testing undo/redo system
-add_executable(test_undo
- test_undo.cc
- ${COMMON_SOURCES}
- ${COMMON_HEADERS}
-)
+if (BUILD_TESTS)
+ # test_undo executable for testing undo/redo system
+ add_executable(test_undo
+ test_undo.cc
+ ${COMMON_SOURCES}
+ ${COMMON_HEADERS}
+ )
-if (KTE_USE_PIECE_TABLE)
- target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
+ if (KTE_USE_PIECE_TABLE)
+ target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
+ endif ()
+
+
+ target_link_libraries(test_undo ${CURSES_LIBRARIES})
endif ()
-target_link_libraries(test_undo ${CURSES_LIBRARIES})
-
if (${BUILD_GUI})
target_sources(kte PRIVATE
Font.h
@@ -142,7 +146,27 @@ if (${BUILD_GUI})
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
- install(TARGETS kge
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
- )
+ # On macOS, build kge as a proper .app bundle
+ if (APPLE)
+ # Configure Info.plist with version and identifiers
+ set(KGE_BUNDLE_ID "dev.kyle.kge")
+ configure_file(
+ ${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
+ ${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
+ @ONLY)
+
+ set_target_properties(kge PROPERTIES
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
+ MACOSX_BUNDLE_BUNDLE_NAME "kge"
+ MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
+
+ install(TARGETS kge
+ BUNDLE DESTINATION .
+ )
+ else()
+ install(TARGETS kge
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ )
+ endif()
endif ()
diff --git a/Command.cc b/Command.cc
index cfcd905..593c208 100644
--- a/Command.cc
+++ b/Command.cc
@@ -1,4 +1,5 @@
#include
+#include
#include "Command.h"
#include "Editor.h"
@@ -418,13 +419,22 @@ cmd_save(CommandContext &ctx)
// non-existent path (not yet file-backed but has a filename).
if (!buf->IsFileBacked()) {
if (!buf->Filename().empty()) {
- if (!buf->SaveAs(buf->Filename(), err)) {
- ctx.editor.SetStatus(err);
- return false;
+ // If first-time save to an existing path, confirm overwrite
+ if (std::filesystem::exists(buf->Filename())) {
+ ctx.editor.StartPrompt(Editor::PromptKind::Confirm, "Overwrite", "");
+ ctx.editor.SetPendingOverwritePath(buf->Filename());
+ ctx.editor.SetStatus(
+ std::string("Overwrite existing file '") + buf->Filename() + "'? (y/N)");
+ return true;
+ } else {
+ if (!buf->SaveAs(buf->Filename(), err)) {
+ ctx.editor.SetStatus(err);
+ return false;
+ }
+ buf->SetDirty(false);
+ ctx.editor.SetStatus("Saved " + buf->Filename());
+ return true;
}
- buf->SetDirty(false);
- ctx.editor.SetStatus("Saved " + buf->Filename());
- return true;
}
// If buffer has no name, prompt for a filename
ctx.editor.StartPrompt(Editor::PromptKind::SaveAs, "Save as", "");
@@ -933,16 +943,52 @@ cmd_newline(CommandContext &ctx)
if (!buf) {
ctx.editor.SetStatus("No buffer to save");
} else {
+ // If this is a first-time save (unnamed/non-file-backed) and the
+ // target exists, ask for confirmation before overwriting.
+ if (!buf->IsFileBacked() && std::filesystem::exists(value)) {
+ ctx.editor.StartPrompt(Editor::PromptKind::Confirm, "Overwrite", "");
+ ctx.editor.SetPendingOverwritePath(value);
+ ctx.editor.SetStatus(
+ std::string("Overwrite existing file '") + value + "'? (y/N)");
+ } else {
+ std::string err;
+ if (!buf->SaveAs(value, err)) {
+ ctx.editor.SetStatus(err);
+ } else {
+ buf->SetDirty(false);
+ ctx.editor.SetStatus("Saved as " + value);
+ if (auto *u = buf->Undo())
+ u->mark_saved();
+ }
+ }
+ }
+ }
+ } else if (kind == Editor::PromptKind::Confirm) {
+ // Confirmation for potentially destructive operations (e.g., overwrite on save-as)
+ Buffer *buf = ctx.editor.CurrentBuffer();
+ const std::string target = ctx.editor.PendingOverwritePath();
+ if (!target.empty() && buf) {
+ bool yes = false;
+ if (!value.empty()) {
+ char c = value[0];
+ yes = (c == 'y' || c == 'Y');
+ }
+ if (yes) {
std::string err;
- if (!buf->SaveAs(value, err)) {
+ if (!buf->SaveAs(target, err)) {
ctx.editor.SetStatus(err);
} else {
buf->SetDirty(false);
- ctx.editor.SetStatus("Saved as " + value);
+ ctx.editor.SetStatus("Saved as " + target);
if (auto *u = buf->Undo())
u->mark_saved();
}
+ } else {
+ ctx.editor.SetStatus("Save canceled");
}
+ ctx.editor.ClearPendingOverwritePath();
+ } else {
+ ctx.editor.SetStatus("Nothing to confirm");
}
}
return true;
@@ -1924,7 +1970,7 @@ cmd_delete_word_prev(CommandContext &ctx)
ensure_cursor_visible(ctx.editor, *buf);
if (!killed_total.empty()) {
if (ctx.editor.KillChain())
- ctx.editor.KillRingAppend(killed_total);
+ ctx.editor.KillRingPrepend(killed_total);
else
ctx.editor.KillRingPush(killed_total);
ctx.editor.SetKillChain(true);
diff --git a/Editor.h b/Editor.h
index ed1b5b1..2b593c2 100644
--- a/Editor.h
+++ b/Editor.h
@@ -99,6 +99,18 @@ public:
}
+ void KillRingPrepend(const std::string &text)
+ {
+ if (text.empty())
+ return;
+ if (kill_ring_.empty()) {
+ KillRingPush(text);
+ } else {
+ kill_ring_.front() = text + kill_ring_.front();
+ }
+ }
+
+
[[nodiscard]] std::string KillRingHead() const
{
return kill_ring_.empty() ? std::string() : kill_ring_.front();
@@ -349,6 +361,25 @@ public:
}
+ // --- Overwrite confirmation (save-as on existing file) ---
+ void SetPendingOverwritePath(const std::string &path)
+ {
+ pending_overwrite_path_ = path;
+ }
+
+
+ void ClearPendingOverwritePath()
+ {
+ pending_overwrite_path_.clear();
+ }
+
+
+ [[nodiscard]] const std::string &PendingOverwritePath() const
+ {
+ return pending_overwrite_path_;
+ }
+
+
[[nodiscard]] const std::string &PromptLabel() const
{
return prompt_label_;
@@ -441,6 +472,7 @@ private:
PromptKind prompt_kind_ = PromptKind::None;
std::string prompt_label_;
std::string prompt_text_;
+ std::string pending_overwrite_path_;
};
#endif // KTE_EDITOR_H
diff --git a/GUIInputHandler.cc b/GUIInputHandler.cc
index 152f795..39df226 100644
--- a/GUIInputHandler.cc
+++ b/GUIInputHandler.cc
@@ -1,5 +1,6 @@
#include
#include
+#include
#include "GUIInputHandler.h"
#include "KKeymap.h"
@@ -26,7 +27,10 @@ map_key(const SDL_Keycode key,
// If previous key was ESC, interpret this as Meta via ESC keymap
if (esc_meta) {
int ascii_key = 0;
- if (key >= SDLK_a && key <= SDLK_z) {
+ if (key == SDLK_BACKSPACE) {
+ // ESC BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
+ ascii_key = KEY_BACKSPACE;
+ } else if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast('a' + (key - SDLK_a));
} else if (key == SDLK_COMMA) {
ascii_key = '<';
@@ -98,14 +102,7 @@ map_key(const SDL_Keycode key,
return true;
case SDLK_ESCAPE:
k_prefix = false;
- esc_meta = true; // next key will be treated as Meta
- // Cancel any universal argument collection
- uarg_active = false;
- uarg_collecting = false;
- uarg_negative = false;
- uarg_had_digits = false;
- uarg_value = 0;
- uarg_text.clear();
+ esc_meta = true; // next key will be treated as Meta
out.hasCommand = false; // no immediate command for bare ESC in GUI
return true;
default:
@@ -222,7 +219,10 @@ map_key(const SDL_Keycode key,
// Alt/Meta bindings (ESC f/b equivalent)
if (is_alt) {
int ascii_key = 0;
- if (key >= SDLK_a && key <= SDLK_z) {
+ if (key == SDLK_BACKSPACE) {
+ // Alt BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
+ ascii_key = KEY_BACKSPACE;
+ } else if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast('a' + (key - SDLK_a));
} else if (key == SDLK_COMMA) {
ascii_key = '<';
diff --git a/TerminalInputHandler.cc b/TerminalInputHandler.cc
index 32df919..b4a683a 100644
--- a/TerminalInputHandler.cc
+++ b/TerminalInputHandler.cc
@@ -85,15 +85,8 @@ map_key_to_command(const int ch,
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
if (ch == 27) {
// ESC
- k_prefix = false;
- esc_meta = true; // next key will be considered meta-modified
- // Cancel any universal argument collection
- uarg_active = false;
- uarg_collecting = false;
- uarg_negative = false;
- uarg_had_digits = false;
- uarg_value = 0;
- uarg_text.clear();
+ k_prefix = false;
+ esc_meta = true; // next key will be considered meta-modified
out.hasCommand = false; // no command yet
return true;
}
diff --git a/TestFrontend.h b/TestFrontend.h
new file mode 100644
index 0000000..e87e610
--- /dev/null
+++ b/TestFrontend.h
@@ -0,0 +1,41 @@
+/*
+ * TestFrontend.h - headless frontend for testing with programmable input
+ */
+#ifndef KTE_TEST_FRONTEND_H
+#define KTE_TEST_FRONTEND_H
+
+#include "Frontend.h"
+#include "TestInputHandler.h"
+#include "TestRenderer.h"
+
+
+class TestFrontend final : public Frontend {
+public:
+ TestFrontend() = default;
+
+ ~TestFrontend() override = default;
+
+ bool Init(Editor &ed) override;
+
+ void Step(Editor &ed, bool &running) override;
+
+ void Shutdown() override;
+
+
+ TestInputHandler &Input()
+ {
+ return input_;
+ }
+
+
+ TestRenderer &Renderer()
+ {
+ return renderer_;
+ }
+
+private:
+ TestInputHandler input_{};
+ TestRenderer renderer_{};
+};
+
+#endif // KTE_TEST_FRONTEND_H
diff --git a/TestInputHandler.h b/TestInputHandler.h
new file mode 100644
index 0000000..25cf4bc
--- /dev/null
+++ b/TestInputHandler.h
@@ -0,0 +1,33 @@
+/*
+ * TestInputHandler.h - programmable input handler for testing
+ */
+#ifndef KTE_TEST_INPUT_HANDLER_H
+#define KTE_TEST_INPUT_HANDLER_H
+
+#include "InputHandler.h"
+#include
+
+
+class TestInputHandler : public InputHandler {
+public:
+ TestInputHandler() = default;
+
+ ~TestInputHandler() override = default;
+
+ bool Poll(MappedInput &out) override;
+
+ void QueueCommand(CommandId id, const std::string &arg = "", int count = 0);
+
+ void QueueText(const std::string &text);
+
+
+ bool IsEmpty() const
+ {
+ return queue_.empty();
+ }
+
+private:
+ std::queue queue_;
+};
+
+#endif // KTE_TEST_INPUT_HANDLER_H
diff --git a/TestRenderer.h b/TestRenderer.h
new file mode 100644
index 0000000..54f3c17
--- /dev/null
+++ b/TestRenderer.h
@@ -0,0 +1,35 @@
+/*
+ * TestRenderer.h - minimal renderer for testing (no actual display)
+ */
+#ifndef KTE_TEST_RENDERER_H
+#define KTE_TEST_RENDERER_H
+
+#include "Renderer.h"
+#include
+
+
+class TestRenderer : public Renderer {
+public:
+ TestRenderer() = default;
+
+ ~TestRenderer() override = default;
+
+ void Draw(Editor &ed) override;
+
+
+ std::size_t GetDrawCount() const
+ {
+ return draw_count_;
+ }
+
+
+ void ResetDrawCount()
+ {
+ draw_count_ = 0;
+ }
+
+private:
+ std::size_t draw_count_ = 0;
+};
+
+#endif // KTE_TEST_RENDERER_H
diff --git a/cmake/packaging.cmake b/cmake/packaging.cmake
deleted file mode 100644
index bca7a00..0000000
--- a/cmake/packaging.cmake
+++ /dev/null
@@ -1,62 +0,0 @@
-# Packaging support
-include(InstallRequiredSystemLibraries)
-
-if (CMAKE_BUILD_TYPE STREQUAL "Debug")
- set(CPACK_DEBIAN_PACKAGE_DEBUG ON)
-endif ()
-
-set(CPACK_PACKAGE_VENDOR "Shimmering Clarity")
-set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "kyle's editor")
-set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
-set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
-set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
-
-###################
-### DEBIANESQUE ###
-###################
-if (${BUILD_GUI})
- set(CPACK_COMPONENTS_ALL gui nox)
-else ()
- set(CPACK_COMPONENTS_ALL nox)
-endif ()
-
-set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP)
-set(CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS ON)
-set(CPACK_DEBIAN_PACKAGE_SECTION universe/editors)
-set(CPACK_DEB_COMPONENT_INSTALL ON)
-
-set(CPACK_DEBIAN_PACKAGE_MAINTAINER "K. Isom")
-set(CPACK_PACKAGE_nox_DESCRIPTION_SUMMARY "kyle's editor")
-set(CPACK_PACKAGE_nox_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION})
-set(CPACK_PACKAGE_nox_PACKAGE_NAME "kte")
-set(CPACK_DEBIAN_nox_PACKAGE_NAME "ke")
-
-if (BUILD_GUI)
- set(CPACK_PACKAGE_gui_PACKAGE_NAME "kge")
- set(CPACK_DEBIAN_gui_PACKAGE_NAME "kge")
- set(CPACK_PACKAGE_gui_DESCRIPTION_SUMMARY " graphical front-end for kyle's editor")
- set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ")
-endif ()
-set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
-set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON)
-
-
-if (LINUX)
- set(CPACK_GENERATOR "DEB;STGZ;TGZ")
-elseif (APPLE)
- set(CPACK_GENERATOR "productbuild;TGZ")
-elseif (MSVC OR MSYS OR MINGW)
- set(CPACK_GENERATOR "NSIS;ZIP")
-else ()
- set(CPACK_GENERATOR "ZIP")
-endif ()
-
-set(CPACK_SOURCE_GENERATOR "TGZ;ZIP ")
-set(CPACK_SOURCE_IGNORE_FILES
- /.git
- /.idea
- /dist
- /.*build.*)
-
-include(CPack)
-cpack_add_component(gui DEPENDS nox)
\ No newline at end of file