diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 68f0866..f1b3720 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -17,7 +17,6 @@
-
@@ -115,33 +114,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 14247dd..0cdb6dc 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -21,7 +21,11 @@
-
+
+
+
+
+
@@ -30,13 +34,17 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -89,54 +97,69 @@
- {
+ "keyToString": {
+ "CMake Application.kge.executor": "Run",
+ "CMake Application.test_example.executor": "Run",
+ "CMake Application.test_undo.executor": "Run",
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
+ "NIXITCH_NIXPKGS_CONFIG": "",
+ "NIXITCH_NIX_CONF_DIR": "",
+ "NIXITCH_NIX_OTHER_STORES": "",
+ "NIXITCH_NIX_PATH": "",
+ "NIXITCH_NIX_PROFILES": "",
+ "NIXITCH_NIX_REMOTE": "",
+ "NIXITCH_NIX_USER_PROFILE_DIR": "",
+ "RunOnceActivity.RadMigrateCodeStyle": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.cidr.known.project.marker": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.readMode.enableVisualFormatting": "true",
+ "RunOnceActivity.west.config.association.type.startup.service": "true",
+ "cf.first.check.clang-format": "false",
+ "cidr.known.project.marker": "true",
+ "code.cleanup.on.save": "true",
+ "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
+ "git-widget-placeholder": "master",
+ "junie.onboarding.icon.badge.shown": "true",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
+ "rearrange.code.on.save": "true",
+ "settings.editor.selected.configurable": "editor.preferences.fonts.default",
+ "to.speed.mode.migration.done": "true",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -153,6 +176,7 @@
+
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
index 636a722..0d3ad62 100644
--- a/.junie/guidelines.md
+++ b/.junie/guidelines.md
@@ -1,39 +1,19 @@
# Project Guidelines
kte is Kyle's Text Editor — a simple, fast text editor written in C++17. It
-replaces the earlier C implementation, ke (see the ke manual in `ke.md`). The
+replaces the earlier C implementation, ke (see the ke manual in `docs/ke.md`). The
design draws inspiration from Antirez' kilo, with keybindings rooted in the
WordStar/VDE family and emacs. The spiritual parent is `mg(1)`.
These guidelines summarize the goals, interfaces, key operations, and current
development practices for kte.
-Style note: all code should be formatted with the current CLion C++ style.
-
## Goals
- Keep the core small, fast, and understandable.
-- Provide an ncurses-based terminal-first editing experience, with an optional ImGui GUI.
+- Provide an ncurses-based terminal-first editing experience, with an additional ImGui GUI.
- Preserve familiar keybindings from ke while modernizing the internals.
-- Favor simple data structures (e.g., gap buffer) and incremental evolution.
-
-## Interfaces
-
-- Command-line interface: the primary interface today.
-- GUI: planned ImGui-based interface.
-
-## Build and Run
-
-Prerequisites: a C++17 compiler, CMake, and ncurses development headers/libs.
-
-- macOS (Homebrew): `brew install ncurses`
-- Debian/Ubuntu: `sudo apt-get install libncurses5-dev libncursesw5-dev`
-
-- Configure and build (example):
- - `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug`
- - `cmake --build cmake-build-debug`
-- Run:
- - `./cmake-build-debug/kte [files]`
+- Favor simple data structures (e.g., piece table) and incremental evolution.
Project entry point: `main.cpp`
@@ -52,31 +32,12 @@ Project entry point: `main.cpp`
## Keybindings (inherited from ke)
-kte aims to maintain ke’s command model while internals evolve. See `ke.md` for
-the full reference. Highlights:
-
-- K-command prefix: `C-k` enters k-command mode; exit with `ESC` or `C-g`.
-- Save/Exit: `C-k s` (save), `C-k x` or `C-k C-x` (save and exit), `C-k q` (quit
- with confirm), `C-k C-q` (quit immediately).
-- Editing: `C-k d` (kill to EOL), `C-k C-d` (kill line), `C-k BACKSPACE` (kill
- to BOL), `C-w` (kill region), `C-y` (yank), `C-u` (universal argument).
-- Navigation/Search: `C-s` (incremental find), `C-r` (regex search), `ESC f/b`
- (word next/prev), `ESC BACKSPACE` (delete previous word).
-- Buffers/Files: `C-k e` (open), `C-k b`/`C-k p` (switch), `C-k c` (close),
- `C-k C-r` (reload).
-- Misc: `C-l` (refresh), `C-g` (cancel), `C-k m` (run make), `C-k g` (goto line).
-
-Known behavior from ke retained for now:
-
-- Incremental search navigates results with arrow keys; search restarts from
- the top on each invocation (known bug to be revisited).
+The file `docs/ke.md` contains the canonical reference for keybindings.
## Contributing/Development Notes
- C++ standard: C++17.
-- Style: match existing file formatting and minimal-comment style.
-- Keep dependencies minimal; ImGui integration will be isolated behind a GUI
- module.
+- Keep dependencies minimal.
- Prefer small, focused changes that preserve ke’s UX unless explicitly changing
behavior.
diff --git a/Buffer.cc b/Buffer.cc
index 8ae25c8..f171f4b 100644
--- a/Buffer.cc
+++ b/Buffer.cc
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
Buffer::Buffer()
@@ -129,12 +130,36 @@ Buffer::operator=(Buffer &&other) noexcept
bool
Buffer::OpenFromFile(const std::string &path, std::string &err)
{
+ auto normalize_path = [](const std::string &in) -> std::string {
+ std::string expanded = in;
+ // Expand leading '~' to HOME
+ if (!expanded.empty() && expanded[0] == '~') {
+ const char *home = std::getenv("HOME");
+ if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\')) {
+ expanded = std::string(home) + expanded.substr(1);
+ } else if (home && expanded.size() == 1) {
+ expanded = std::string(home);
+ }
+ }
+ try {
+ std::filesystem::path p(expanded);
+ if (std::filesystem::exists(p)) {
+ return std::filesystem::canonical(p).string();
+ }
+ return std::filesystem::absolute(p).string();
+ } catch (...) {
+ // On any error, fall back to input
+ return expanded;
+ }
+ };
+
+ const std::string norm = normalize_path(path);
// If the file doesn't exist, initialize an empty, non-file-backed buffer
// with the provided filename. Do not touch the filesystem until Save/SaveAs.
- if (!std::filesystem::exists(path)) {
+ if (!std::filesystem::exists(norm)) {
rows_.clear();
nrows_ = 0;
- filename_ = path;
+ filename_ = norm;
is_file_backed_ = false;
dirty_ = false;
@@ -147,9 +172,9 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
return true;
}
- std::ifstream in(path, std::ios::in | std::ios::binary);
+ std::ifstream in(norm, std::ios::in | std::ios::binary);
if (!in) {
- err = "Failed to open file: " + path;
+ err = "Failed to open file: " + norm;
return false;
}
@@ -194,7 +219,7 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
}
nrows_ = rows_.size();
- filename_ = path;
+ filename_ = norm;
is_file_backed_ = true;
dirty_ = false;
@@ -250,10 +275,29 @@ Buffer::Save(std::string &err) const
bool
Buffer::SaveAs(const std::string &path, std::string &err)
{
+ // Normalize output path first
+ std::string out_path;
+ try {
+ std::filesystem::path p(path);
+ // Do a light expansion of '~'
+ std::string expanded = path;
+ if (!expanded.empty() && expanded[0] == '~') {
+ const char *home = std::getenv("HOME");
+ if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\'))
+ expanded = std::string(home) + expanded.substr(1);
+ else if (home && expanded.size() == 1)
+ expanded = std::string(home);
+ }
+ std::filesystem::path ep(expanded);
+ out_path = std::filesystem::absolute(ep).string();
+ } catch (...) {
+ out_path = path;
+ }
+
// Write to the given path
- std::ofstream out(path, std::ios::out | std::ios::binary | std::ios::trunc);
+ std::ofstream out(out_path, std::ios::out | std::ios::binary | std::ios::trunc);
if (!out) {
- err = "Failed to open for write: " + path;
+ err = "Failed to open for write: " + out_path;
return false;
}
for (std::size_t i = 0; i < rows_.size(); ++i) {
@@ -270,7 +314,7 @@ Buffer::SaveAs(const std::string &path, std::string &err)
return false;
}
- filename_ = path;
+ filename_ = out_path;
is_file_backed_ = true;
dirty_ = false;
return true;
diff --git a/Command.cc b/Command.cc
index bd3b35e..486b37e 100644
--- a/Command.cc
+++ b/Command.cc
@@ -1,5 +1,6 @@
#include
#include
+#include
#include "Command.h"
#include "Editor.h"
@@ -453,6 +454,36 @@ cmd_save(CommandContext &ctx)
}
+// --- Working directory commands ---
+static bool
+cmd_show_working_directory(CommandContext &ctx)
+{
+ try {
+ std::filesystem::path cwd = std::filesystem::current_path();
+ ctx.editor.SetStatus(std::string("cwd: ") + cwd.string());
+ return true;
+ } catch (const std::exception &e) {
+ ctx.editor.SetStatus(std::string("cwd: ") + e.what());
+ return false;
+ }
+}
+
+
+static bool
+cmd_change_working_directory_start(CommandContext &ctx)
+{
+ std::string initial;
+ try {
+ initial = std::filesystem::current_path().string();
+ } catch (...) {
+ initial.clear();
+ }
+ ctx.editor.StartPrompt(Editor::PromptKind::Chdir, "chdir", initial);
+ ctx.editor.SetStatus(std::string("chdir: ") + ctx.editor.PromptText());
+ return true;
+}
+
+
static bool
cmd_save_as(CommandContext &ctx)
{
@@ -762,43 +793,141 @@ cmd_insert_text(CommandContext &ctx)
}
// If a prompt is active, edit prompt text
if (ctx.editor.PromptActive()) {
- // Special-case: buffer switch prompt supports Tab-completion
- if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::BufferSwitch && ctx.arg == "\t") {
- // Complete against buffer names (path and basename)
- const std::string prefix = ctx.editor.PromptText();
- std::vector > cands; // name, index
- const auto &bs = ctx.editor.Buffers();
- for (std::size_t i = 0; i < bs.size(); ++i) {
- std::string full = buffer_display_name(bs[i]);
- std::string base = buffer_basename(bs[i]);
- if (full.rfind(prefix, 0) == 0) {
- cands.emplace_back(full, i);
+ // Special-case: Tab-completion for prompts
+ if (ctx.arg == "\t") {
+ auto kind = ctx.editor.CurrentPromptKind();
+ // Buffer switch prompt supports Tab-completion on buffer names
+ if (kind == Editor::PromptKind::BufferSwitch) {
+ // Complete against buffer names (path and basename)
+ const std::string prefix = ctx.editor.PromptText();
+ std::vector > cands; // name, index
+ const auto &bs = ctx.editor.Buffers();
+ for (std::size_t i = 0; i < bs.size(); ++i) {
+ std::string full = buffer_display_name(bs[i]);
+ std::string base = buffer_basename(bs[i]);
+ if (full.rfind(prefix, 0) == 0) {
+ cands.emplace_back(full, i);
+ }
+ if (base.rfind(prefix, 0) == 0 && base != full) {
+ cands.emplace_back(base, i);
+ }
}
- if (base.rfind(prefix, 0) == 0 && base != full) {
- cands.emplace_back(base, i);
+ if (cands.empty()) {
+ // no change
+ } else if (cands.size() == 1) {
+ ctx.editor.SetPromptText(cands[0].first);
+ } else {
+ // extend to longest common prefix
+ std::string lcp = cands[0].first;
+ for (std::size_t i = 1; i < cands.size(); ++i) {
+ const std::string &s = cands[i].first;
+ std::size_t j = 0;
+ while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
+ ++j;
+ lcp.resize(j);
+ if (lcp.empty())
+ break;
+ }
+ if (!lcp.empty() && lcp != ctx.editor.PromptText())
+ ctx.editor.SetPromptText(lcp);
}
+ ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
+ return true;
}
- if (cands.empty()) {
- // no change
- } else if (cands.size() == 1) {
- ctx.editor.SetPromptText(cands[0].first);
- } else {
- // extend to longest common prefix
- std::string lcp = cands[0].first;
- for (std::size_t i = 1; i < cands.size(); ++i) {
- const std::string &s = cands[i].first;
- std::size_t j = 0;
- while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
- ++j;
- lcp.resize(j);
- if (lcp.empty())
- break;
+
+ // File path completion for OpenFile/SaveAs/Chdir
+ if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs
+ || kind == Editor::PromptKind::Chdir) {
+ auto expand_user_path = [](const std::string &in) -> std::string {
+ if (!in.empty() && in[0] == '~') {
+ const char *home = std::getenv("HOME");
+ if (home && in.size() == 1)
+ return std::string(home);
+ if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) {
+ std::string rest = in.substr(1); // keep leading slash
+ return std::string(home) + rest;
+ }
+ }
+ return in;
+ };
+
+ std::string text = ctx.editor.PromptText();
+ // Build a path and split dir + base prefix
+ std::string expanded = expand_user_path(text);
+ std::filesystem::path p(expanded);
+ std::filesystem::path dir;
+ std::string base;
+ if (expanded.empty()) {
+ dir = std::filesystem::current_path();
+ base.clear();
+ } else if (std::filesystem::is_directory(p)) {
+ dir = p;
+ base.clear();
+ } else {
+ dir = p.parent_path();
+ base = p.filename().string();
+ if (dir.empty())
+ dir = std::filesystem::current_path();
}
- if (!lcp.empty() && lcp != ctx.editor.PromptText())
- ctx.editor.SetPromptText(lcp);
+
+ std::error_code ec;
+ std::vector entries;
+ std::filesystem::directory_iterator it(dir, ec), end;
+ for (; !ec && it != end; it.increment(ec)) {
+ entries.push_back(*it);
+ }
+ // Filter by base prefix
+ std::vector cands;
+ for (const auto &de: entries) {
+ std::string name = de.path().filename().string();
+ if (base.empty() || name.rfind(base, 0) == 0) {
+ std::string candidate = (dir / name).string();
+ // For dirs, add trailing slash hint
+ if (de.is_directory(ec))
+ candidate += "/";
+ cands.push_back(candidate);
+ }
+ }
+ // If no candidates, keep as-is
+ if (cands.empty()) {
+ // no-op
+ } else if (cands.size() == 1) {
+ ctx.editor.SetPromptText(cands[0]);
+ } else {
+ // Longest common prefix of display strings
+ auto lcp = cands[0];
+ for (size_t i = 1; i < cands.size(); ++i) {
+ const auto &s = cands[i];
+ size_t j = 0;
+ while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
+ ++j;
+ lcp.resize(j);
+ if (lcp.empty())
+ break;
+ }
+ if (!lcp.empty() && lcp != ctx.editor.PromptText()) {
+ ctx.editor.SetPromptText(lcp);
+ } else {
+ // Show some choices in status (trim to avoid spam)
+ std::string msg = ctx.editor.PromptLabel() + ": ";
+ size_t shown = 0;
+ for (const auto &s: cands) {
+ if (shown >= 10) {
+ msg += " …";
+ break;
+ }
+ if (shown > 0)
+ msg += ' ';
+ msg += std::filesystem::path(s).filename().string();
+ ++shown;
+ }
+ ctx.editor.SetStatus(msg);
+ return true;
+ }
+ }
+ ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
+ return true;
}
- ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
- return true;
}
ctx.editor.AppendPromptText(ctx.arg);
@@ -909,6 +1038,20 @@ cmd_newline(CommandContext &ctx)
ensure_cursor_visible(ctx.editor, *b);
} else if (kind == Editor::PromptKind::OpenFile) {
std::string err;
+ // Expand "~" to the user's home directory
+ auto expand_user_path = [](const std::string &in) -> std::string {
+ if (!in.empty() && in[0] == '~') {
+ const char *home = std::getenv("HOME");
+ if (home && in.size() == 1)
+ return std::string(home);
+ if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) {
+ std::string rest = in.substr(1);
+ return std::string(home) + rest;
+ }
+ }
+ return in;
+ };
+ value = expand_user_path(value);
if (value.empty()) {
ctx.editor.SetStatus("Open canceled (empty)");
} else if (!ctx.editor.OpenFile(value, err)) {
@@ -953,6 +1096,21 @@ cmd_newline(CommandContext &ctx)
if (!buf) {
ctx.editor.SetStatus("No buffer to save");
} else {
+ // Expand "~" for save path
+ auto expand_user_path = [](const std::string &in) -> std::string {
+ if (!in.empty() && in[0] == '~') {
+ const char *home = std::getenv("HOME");
+ if (home && in.size() == 1)
+ return std::string(home);
+ if (home && (in.size() > 1) && (
+ in[1] == '/' || in[1] == '\\')) {
+ std::string rest = in.substr(1);
+ return std::string(home) + rest;
+ }
+ }
+ return in;
+ };
+ value = expand_user_path(value);
// 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)) {
@@ -1031,6 +1189,43 @@ cmd_newline(CommandContext &ctx)
buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf);
ctx.editor.SetStatus("Goto line " + std::to_string(line1));
+ } else if (kind == Editor::PromptKind::Chdir) {
+ // Attempt to change the current working directory
+ if (value.empty()) {
+ ctx.editor.SetStatus("chdir canceled (empty)");
+ return true;
+ }
+ try {
+ // Expand "~" for chdir
+ auto expand_user_path = [](const std::string &in) -> std::string {
+ if (!in.empty() && in[0] == '~') {
+ const char *home = std::getenv("HOME");
+ if (home && in.size() == 1)
+ return std::string(home);
+ if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) {
+ std::string rest = in.substr(1);
+ return std::string(home) + rest;
+ }
+ }
+ return in;
+ };
+ value = expand_user_path(value);
+ std::filesystem::path p(value);
+ std::error_code ec;
+ // Expand if value is relative: resolve against current_path implicitly
+ if (!std::filesystem::exists(p, ec)) {
+ ctx.editor.SetStatus(std::string("chdir: no such path: ") + value);
+ return true;
+ }
+ if (!std::filesystem::is_directory(p, ec)) {
+ ctx.editor.SetStatus(std::string("chdir: not a directory: ") + value);
+ return true;
+ }
+ std::filesystem::current_path(p);
+ ctx.editor.SetStatus(std::string("cwd: ") + std::filesystem::current_path().string());
+ } catch (const std::exception &e) {
+ ctx.editor.SetStatus(std::string("chdir failed: ") + e.what());
+ }
}
return true;
}
@@ -2435,6 +2630,15 @@ InstallDefaultCommands()
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
cmd_mark_all_and_jump_end
});
+ // Working directory
+ CommandRegistry::Register({
+ CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
+ cmd_show_working_directory
+ });
+ CommandRegistry::Register({
+ CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
+ cmd_change_working_directory_start
+ });
// UI helpers
CommandRegistry::Register(
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
diff --git a/Command.h b/Command.h
index c428070..253faa6 100644
--- a/Command.h
+++ b/Command.h
@@ -74,6 +74,8 @@ enum class CommandId {
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
// Direct navigation by line number
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.
// Meta
UnknownKCommand, // arg: single character that was not recognized after C-k
};
diff --git a/Editor.cc b/Editor.cc
index be691b2..825d928 100644
--- a/Editor.cc
+++ b/Editor.cc
@@ -2,6 +2,7 @@
#include
#include
+#include
Editor::Editor() = default;
@@ -43,6 +44,78 @@ Editor::CurrentBuffer() const
}
+static std::vector
+split_reverse(const std::filesystem::path &p)
+{
+ std::vector parts;
+ for (auto it = p; !it.empty(); it = it.parent_path()) {
+ if (it == it.parent_path()) {
+ // root or single element
+ if (!it.empty())
+ parts.push_back(it);
+ break;
+ }
+ parts.push_back(it.filename());
+ }
+ return parts; // from leaf toward root
+}
+
+
+std::string
+Editor::DisplayNameFor(const Buffer &buf) const
+{
+ std::string full = buf.Filename();
+ if (full.empty())
+ return std::string("[no name]");
+
+ std::filesystem::path target(full);
+ auto target_parts = split_reverse(target);
+ if (target_parts.empty())
+ return target.filename().string();
+
+ // Prepare list of other buffer paths
+ std::vector > others;
+ others.reserve(buffers_.size());
+ for (const auto &b: buffers_) {
+ if (&b == &buf)
+ continue;
+ if (b.Filename().empty())
+ continue;
+ others.push_back(split_reverse(std::filesystem::path(b.Filename())));
+ }
+
+ // Increase suffix length until unique among others
+ std::size_t need = 1; // at least basename
+ for (;;) {
+ // Build candidate suffix for target
+ std::filesystem::path cand;
+ for (std::size_t i = 0; i < need && i < target_parts.size(); ++i) {
+ cand = std::filesystem::path(target_parts[i]) / cand;
+ }
+ // Compare against others
+ bool clash = false;
+ for (const auto &o_parts: others) {
+ std::filesystem::path ocand;
+ for (std::size_t i = 0; i < need && i < o_parts.size(); ++i) {
+ ocand = std::filesystem::path(o_parts[i]) / ocand;
+ }
+ if (ocand == cand) {
+ clash = true;
+ break;
+ }
+ }
+ if (!clash || need >= target_parts.size()) {
+ std::string s = cand.string();
+ // Remove any trailing slash that may appear from root joining
+ if (!s.empty() && (s.back() == '/' || s.back() == '\\'))
+ s.pop_back();
+ return s;
+ }
+ ++need;
+ }
+}
+
+
std::size_t
Editor::AddBuffer(const Buffer &buf)
{
diff --git a/Editor.h b/Editor.h
index 4c42716..ad2aeb9 100644
--- a/Editor.h
+++ b/Editor.h
@@ -302,7 +302,7 @@ public:
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
- enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine };
+ enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
@@ -409,6 +409,11 @@ public:
const Buffer *CurrentBuffer() const;
+ // Compute a display-friendly short name for a buffer path that is the
+ // shortest unique suffix among all open buffers. If buffer has no name,
+ // returns "[no name]".
+ [[nodiscard]] std::string DisplayNameFor(const Buffer &buf) const;
+
// Add an existing buffer (copy/move) or open from file path
std::size_t AddBuffer(const Buffer &buf);
diff --git a/GUIRenderer.cc b/GUIRenderer.cc
index 8678290..236490f 100644
--- a/GUIRenderer.cc
+++ b/GUIRenderer.cc
@@ -204,18 +204,26 @@ GUIRenderer::Draw(Editor &ed)
left += "kge"; // GUI app name
left += " ";
left += KTE_VERSION_STR;
- std::string fname = buf->Filename();
- if (!fname.empty()) {
+ std::string fname;
+ try {
+ fname = ed.DisplayNameFor(*buf);
+ } catch (...) {
+ fname = buf->Filename();
try {
fname = std::filesystem::path(fname).filename().string();
} catch (...) {}
- } else {
- fname = "[no name]";
}
left += " ";
left += fname;
if (buf->Dirty())
left += " *";
+ // Append total line count as "L"
+ {
+ unsigned long lcount = static_cast(buf->Rows().size());
+ left += " ";
+ left += std::to_string(lcount);
+ left += "L";
+ }
// Build right text (cursor/mark)
int row1 = static_cast(buf->Cury()) + 1;
diff --git a/KKeymap.cc b/KKeymap.cc
index 7e2b36f..a1ba404 100644
--- a/KKeymap.cc
+++ b/KKeymap.cc
@@ -13,13 +13,13 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
switch (k_lower) {
case 'd':
out = CommandId::KillLine;
- return true; // C-k C-d
- case 'x':
- out = CommandId::SaveAndQuit;
- return true; // C-k C-x
+ return true;
case 'q':
out = CommandId::QuitNow;
- return true; // C-k C-q (quit immediately)
+ return true;
+ case 'x':
+ out = CommandId::SaveAndQuit;
+ return true;
default:
// Important: do not return here — fall through to non-ctrl table
// so that C-k u/U still work even if Ctrl is (incorrectly) held
@@ -33,65 +33,72 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
return true;
}
- // 3) Non-control k-table (lowercased)
switch (k_lower) {
- case 'j':
- out = CommandId::JumpToMark;
- return true; // C-k j
- case 'f':
- out = CommandId::FlushKillRing;
- return true; // C-k f
- case 'd':
- out = CommandId::KillToEOL;
- return true; // C-k d
- case 'y':
- out = CommandId::Yank;
- return true; // C-k y
- case 's':
- out = CommandId::Save;
- return true; // C-k s
- case 'e':
- out = CommandId::OpenFileStart;
- return true; // C-k e (open file)
- case 'b':
- out = CommandId::BufferSwitchStart;
- return true; // C-k b (switch buffer by name)
- case 'c':
- out = CommandId::BufferClose;
- return true; // C-k c (close current buffer)
- case 'n':
- out = CommandId::BufferPrev;
- return true; // C-k n (switch to previous buffer)
- case 'x':
- out = CommandId::SaveAndQuit;
- return true; // C-k x
- case 'q':
- out = CommandId::Quit;
- return true; // C-k q
- case 'p':
- out = CommandId::BufferNext;
- return true; // C-k p (switch to next buffer)
- case 'u':
- out = CommandId::Undo;
- return true; // C-k u (undo)
- case '-':
- out = CommandId::UnindentRegion;
- return true; // C-k - (unindent region)
- case '=':
- out = CommandId::IndentRegion;
- return true; // C-k = (indent region)
- case 'l':
- out = CommandId::ReloadBuffer;
- return true; // C-k l (reload buffer)
case 'a':
out = CommandId::MarkAllAndJumpEnd;
- return true; // C-k a (mark all and jump to end)
+ return true;
+ case 'b':
+ out = CommandId::BufferSwitchStart;
+ return true;
+ case 'c':
+ out = CommandId::BufferClose;
+ return true;
+ case 'd':
+ out = CommandId::KillToEOL;
+ return true;
+ case 'e':
+ out = CommandId::OpenFileStart;
+ return true;
+ case 'f':
+ out = CommandId::FlushKillRing;
+ return true;
case 'g':
out = CommandId::JumpToLine;
- return true; // C-k g (goto line)
+ return true;
+ case 'j':
+ out = CommandId::JumpToMark;
+ return true;
+ case 'l':
+ out = CommandId::ReloadBuffer;
+ return true;
+ case 'n':
+ out = CommandId::BufferPrev;
+ return true;
+ case 'o':
+ out = CommandId::ChangeWorkingDirectory;
+ return true;
+ case 'p':
+ out = CommandId::BufferNext;
+ return true;
+ case 'q':
+ out = CommandId::Quit;
+ return true;
+ case 's':
+ out = CommandId::Save;
+ return true;
+ case 'u':
+ out = CommandId::Undo;
+ return true;
+ case 'w':
+ out = CommandId::ShowWorkingDirectory;
+ return true;
+ case 'x':
+ out = CommandId::SaveAndQuit;
+ return true;
+ case 'y':
+ out = CommandId::Yank;
+ return true;
+ case '-':
+ out = CommandId::UnindentRegion;
+ return true;
+ case '=':
+ out = CommandId::IndentRegion;
+ return true;
default:
break;
}
+
+ // 3) Non-control k-table (lowercased)
return false;
}
diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc
index a15dfff..9ecdcfc 100644
--- a/TerminalRenderer.cc
+++ b/TerminalRenderer.cc
@@ -168,13 +168,13 @@ TerminalRenderer::Draw(Editor &ed)
const Buffer *b = buf;
std::string fname;
if (b) {
- fname = b->Filename();
- }
- if (!fname.empty()) {
try {
- fname = std::filesystem::path(fname).filename().string();
+ fname = ed.DisplayNameFor(*b);
} catch (...) {
- // keep original on any error
+ fname = b->Filename();
+ try {
+ fname = std::filesystem::path(fname).filename().string();
+ } catch (...) {}
}
} else {
fname = "[no name]";
@@ -183,6 +183,13 @@ TerminalRenderer::Draw(Editor &ed)
left += fname;
if (b && b->Dirty())
left += " *";
+ // Append total line count as "L"
+ if (b) {
+ unsigned long lcount = static_cast(b->Rows().size());
+ left += " ";
+ left += std::to_string(lcount);
+ left += "L";
+ }
}
// Build right segment (cursor and mark)
diff --git a/main.cc b/main.cc
index fcd6fb5..d3ca77a 100644
--- a/main.cc
+++ b/main.cc
@@ -26,52 +26,53 @@ static void
PrintUsage(const char *prog)
{
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
- << "Options:\n"
- << " -g, --gui Use GUI frontend (if built)\n"
- << " -t, --term Use terminal (ncurses) frontend [default]\n"
- << " -h, --help Show this help and exit\n"
- << " -V, --version Show version and exit\n";
+ << "Options:\n"
+ << " -g, --gui Use GUI frontend (if built)\n"
+ << " -t, --term Use terminal (ncurses) frontend [default]\n"
+ << " -h, --help Show this help and exit\n"
+ << " -V, --version Show version and exit\n";
}
+
int
main(int argc, const char *argv[])
{
Editor editor;
// CLI parsing using getopt_long
- bool req_gui = false;
- bool req_term = false;
- bool show_help = false;
+ bool req_gui = false;
+ bool req_term = false;
+ bool show_help = false;
bool show_version = false;
static struct option long_opts[] = {
- {"gui", no_argument, nullptr, 'g'},
- {"term", no_argument, nullptr, 't'},
- {"help", no_argument, nullptr, 'h'},
- {"version", no_argument, nullptr, 'V'},
- {nullptr, 0, nullptr, 0}
+ {"gui", no_argument, nullptr, 'g'},
+ {"term", no_argument, nullptr, 't'},
+ {"help", no_argument, nullptr, 'h'},
+ {"version", no_argument, nullptr, 'V'},
+ {nullptr, 0, nullptr, 0}
};
int opt;
int long_index = 0;
while ((opt = getopt_long(argc, const_cast(argv), "gthV", long_opts, &long_index)) != -1) {
switch (opt) {
- case 'g':
- req_gui = true;
- break;
- case 't':
- req_term = true;
- break;
- case 'h':
- show_help = true;
- break;
- case 'V':
- show_version = true;
- break;
- case '?':
- default:
- PrintUsage(argv[0]);
- return 2;
+ case 'g':
+ req_gui = true;
+ break;
+ case 't':
+ req_term = true;
+ break;
+ case 'h':
+ show_help = true;
+ break;
+ case 'V':
+ show_version = true;
+ break;
+ case '?':
+ default:
+ PrintUsage(argv[0]);
+ return 2;
}
}
@@ -92,7 +93,7 @@ main(int argc, const char *argv[])
#if !defined(KTE_BUILD_GUI)
if (req_gui) {
std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed." <<
- std::endl;
+ std::endl;
return 2;
}
#else
@@ -132,7 +133,7 @@ main(int argc, const char *argv[])
// Clamp to >=1 later; 0 disables.
try {
unsigned long v = std::stoul(p);
- pending_line = static_cast(v);
+ pending_line = static_cast(v);
} catch (...) {
// Ignore malformed huge numbers
pending_line = 0;
@@ -152,7 +153,7 @@ main(int argc, const char *argv[])
// Apply pending +N to the just-opened (current) buffer
if (Buffer *b = editor.CurrentBuffer()) {
std::size_t nrows = b->Nrows();
- std::size_t line = pending_line > 0 ? pending_line - 1 : 0;
+ std::size_t line = pending_line > 0 ? pending_line - 1 : 0;
// 1-based to 0-based
if (nrows > 0) {
if (line >= nrows)
@@ -178,7 +179,7 @@ main(int argc, const char *argv[])
InstallDefaultCommands();
// Select frontend
- std::unique_ptr fe;
+ std::unique_ptr fe;
#if defined(KTE_BUILD_GUI)
if (use_gui) {
fe = std::make_unique();