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 @@ - - - - - - + + + + - - - - - + + - - - - - + - + - - - - - @@ -187,7 +171,7 @@ @@ -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