tui_editor.cpp 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182
  1. #include "lumacs/tui_editor.hpp"
  2. #include "lumacs/editor_core.hpp"
  3. #include "lumacs/lua_api.hpp"
  4. #include <ncurses.h>
  5. #include <iostream>
  6. #include <fstream>
  7. #include <memory>
  8. #include <chrono>
  9. #include <string>
  10. #include <sstream>
  11. #include <algorithm>
  12. // Global debug log
  13. extern std::ofstream debug_log;
  14. std::ofstream debug_log("lumacs_debug.log");
  15. using namespace lumacs;
  16. /// ncurses-based TUI frontend for Lumacs
  17. class TuiEditor : public IEditorView {
  18. public:
  19. TuiEditor() : core_(nullptr) {}
  20. ~TuiEditor() override {
  21. endwin(); // Cleanup ncurses
  22. }
  23. // IEditorView methods
  24. void init() override;
  25. void run() override;
  26. void handle_editor_event(EditorEvent event) override;
  27. void set_core(EditorCore* core) override;
  28. private:
  29. enum class Mode {
  30. Normal,
  31. Command, // Minibuffer entry
  32. FindFile, // Find file prompt
  33. BufferSwitch, // Buffer switching with completion
  34. KillBuffer, // Kill buffer with completion
  35. ConfirmKill, // Confirm killing modified buffer
  36. ISearch // Incremental search
  37. };
  38. EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
  39. bool should_quit_ = false;
  40. std::string message_line_;
  41. int height_ = 0, width_ = 0;
  42. // Input state
  43. Mode mode_ = Mode::Normal;
  44. std::string command_buffer_;
  45. // ISearch state
  46. std::string isearch_query_;
  47. bool isearch_forward_ = true;
  48. Position isearch_start_pos_;
  49. std::optional<Range> isearch_match_;
  50. bool isearch_failed_ = false;
  51. // Completion state
  52. std::vector<std::string> completion_candidates_;
  53. size_t completion_index_ = 0;
  54. std::string completion_prefix_;
  55. // Minibuffer history
  56. std::vector<std::string> command_history_;
  57. std::vector<std::string> buffer_switch_history_;
  58. std::vector<std::string> kill_buffer_history_;
  59. std::vector<std::string> isearch_history_;
  60. size_t history_index_ = 0;
  61. // Meta key handling
  62. bool waiting_for_meta_ = false;
  63. std::chrono::steady_clock::time_point meta_time_;
  64. static constexpr auto META_TIMEOUT = std::chrono::milliseconds(100);
  65. // Private helper method declarations
  66. std::string resolve_key(int ch);
  67. std::vector<std::string>& get_current_history();
  68. void add_to_history(const std::string& entry);
  69. void previous_history();
  70. void next_history();
  71. void reset_history_navigation();
  72. void update_completion_candidates(const std::string& prefix);
  73. void reset_completion();
  74. void perform_search(bool find_next);
  75. bool handle_input(int ch);
  76. bool process_key(const std::string& key_name);
  77. void execute_command(const std::string& cmd);
  78. int get_attributes_for_face(const std::string& face_name);
  79. void render();
  80. void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
  81. void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
  82. void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
  83. void render_status_line();
  84. void render_message_line();
  85. };
  86. // --- TuiEditor Public Method Definitions ---
  87. void TuiEditor::init() {
  88. // Initialize ncurses
  89. initscr();
  90. cbreak(); // Disable line buffering
  91. noecho(); // Don't echo pressed keys
  92. keypad(stdscr, TRUE); // Enable special keys
  93. raw(); // Enable all control characters
  94. timeout(50); // Set 50ms timeout for getch() to avoid blocking forever
  95. // Color support
  96. if (has_colors()) {
  97. start_color();
  98. use_default_colors();
  99. }
  100. // Get screen dimensions
  101. getmaxyx(stdscr, height_, width_);
  102. // Initialize theme colors for ncurses
  103. if (has_colors() && core_->active_theme()) {
  104. core_->active_theme()->initialize_ncurses_colors();
  105. }
  106. // Set initial viewport size (leave room for status and message lines)
  107. int content_height = height_ - 2; // -1 for status, -1 for message
  108. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  109. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  110. int content_width = width_ - line_number_width;
  111. core_->set_viewport_size(content_width, content_height);
  112. debug_log << "ncurses editor initialized: " << width_ << "x" << height_ << std::endl;
  113. }
  114. void TuiEditor::run() {
  115. should_quit_ = false;
  116. // Initial render
  117. render();
  118. while (!should_quit_) {
  119. // Handle screen resize
  120. int new_height, new_width;
  121. getmaxyx(stdscr, new_height, new_width);
  122. if (new_height != height_ || new_width != width_) {
  123. height_ = new_height;
  124. width_ = new_width;
  125. int content_height = height_ - 2;
  126. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  127. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  128. int content_width = width_ - line_number_width;
  129. core_->set_viewport_size(content_width, content_height);
  130. debug_log << "Screen resized to: " << width_ << "x" << height_ << std::endl;
  131. debug_log << "Content area: " << content_width << "x" << content_height << std::endl;
  132. render(); // Re-render after resize
  133. }
  134. // Get input (with timeout)
  135. int ch = getch();
  136. // Only process input and render if we got actual input (not timeout)
  137. if (ch != ERR) {
  138. handle_input(ch);
  139. render();
  140. }
  141. }
  142. }
  143. void TuiEditor::handle_editor_event(EditorEvent event) { // existing method
  144. if (event == EditorEvent::Quit) {
  145. should_quit_ = true;
  146. } else if (event == EditorEvent::Message) {
  147. message_line_ = core_->last_message();
  148. } else if (event == EditorEvent::CommandMode) {
  149. mode_ = Mode::Command;
  150. command_buffer_.clear();
  151. reset_completion();
  152. reset_history_navigation();
  153. } else if (event == EditorEvent::BufferSwitchMode) {
  154. mode_ = Mode::BufferSwitch;
  155. command_buffer_.clear();
  156. reset_completion();
  157. reset_history_navigation();
  158. } else if (event == EditorEvent::KillBufferMode) {
  159. mode_ = Mode::KillBuffer;
  160. command_buffer_.clear();
  161. reset_completion();
  162. reset_history_navigation();
  163. } else if (event == EditorEvent::FindFileMode) {
  164. mode_ = Mode::FindFile;
  165. command_buffer_.clear();
  166. reset_completion();
  167. reset_history_navigation();
  168. } else if (event == EditorEvent::ISearchMode) {
  169. mode_ = Mode::ISearch;
  170. isearch_query_.clear();
  171. isearch_forward_ = true;
  172. isearch_start_pos_ = core_->cursor();
  173. isearch_match_ = std::nullopt;
  174. isearch_failed_ = false;
  175. } else if (event == EditorEvent::ISearchBackwardMode) {
  176. mode_ = Mode::ISearch;
  177. isearch_query_.clear();
  178. isearch_forward_ = false;
  179. isearch_start_pos_ = core_->cursor();
  180. isearch_match_ = std::nullopt;
  181. isearch_failed_ = false;
  182. }
  183. }
  184. void TuiEditor::set_core(EditorCore* core) {
  185. core_ = core;
  186. }
  187. // --- TuiEditor Private Helper Method Definitions ---
  188. /// Convert ncurses key code to our key name format
  189. std::string TuiEditor::resolve_key(int ch) {
  190. debug_log << "=== NCURSES INPUT DEBUG ===" << std::endl;
  191. debug_log << "Raw key code: " << ch << " (0x" << std::hex << ch << std::dec << ")" << std::endl;
  192. std::string key_name;
  193. // Handle special ncurses key codes first
  194. if (ch >= KEY_MIN) {
  195. switch (ch) {
  196. case KEY_UP: key_name = "ArrowUp"; break;
  197. case KEY_DOWN: key_name = "ArrowDown"; break;
  198. case KEY_LEFT: key_name = "ArrowLeft"; break;
  199. case KEY_RIGHT: key_name = "ArrowRight"; break;
  200. case KEY_HOME: key_name = "Home"; break;
  201. case KEY_END: key_name = "End"; break;
  202. case KEY_BACKSPACE: key_name = "Backspace"; break;
  203. case KEY_DC: key_name = "Delete"; break;
  204. case KEY_ENTER: key_name = "Return"; break;
  205. case KEY_F(3): key_name = "F3"; break;
  206. case KEY_F(4): key_name = "F4"; break;
  207. default:
  208. debug_log << "Unknown special key: " << ch << " (ignoring)" << std::endl;
  209. // Return empty string to ignore unknown special keys
  210. return "";
  211. }
  212. } else {
  213. // Handle normal ASCII characters and control codes
  214. switch (ch) {
  215. case 127: // DEL
  216. case 8: // BS
  217. key_name = "Backspace"; break;
  218. case '\n':
  219. case '\r':
  220. key_name = "Return"; break;
  221. case '\t':
  222. key_name = "Tab"; break;
  223. case 27:
  224. key_name = "Escape"; break;
  225. default:
  226. // Control characters (1-26, excluding special cases)
  227. if (ch >= 1 && ch >= 1 && ch <= 26 && ch != 8 && ch != 9 && ch != 10 && ch != 13) {
  228. char letter = 'a' + (ch - 1);
  229. key_name = "C-" + std::string(1, letter);
  230. debug_log << "Control character detected: " << ch << " -> " << key_name << std::endl;
  231. }
  232. // Printable ASCII characters
  233. else if (ch >= 32 && ch <= 126) {
  234. key_name = std::string(1, static_cast<char>(ch));
  235. }
  236. // Extended characters (might be Meta combinations)
  237. else if (ch >= 128 && ch < 256) {
  238. char base_char = ch - 128;
  239. if (base_char >= 32 && base_char <= 126) {
  240. key_name = "M-" + std::string(1, base_char);
  241. }
  242. }
  243. else {
  244. debug_log << "Unhandled character code: " << ch << std::endl;
  245. }
  246. }
  247. }
  248. debug_log << "Resolved key: '" << key_name << "'" << std::endl;
  249. debug_log << "============================" << std::endl;
  250. return key_name;
  251. }
  252. // History management
  253. std::vector<std::string>& TuiEditor::get_current_history() {
  254. switch (mode_) {
  255. case Mode::Command: return command_history_;
  256. case Mode::BufferSwitch: return buffer_switch_history_;
  257. case Mode::KillBuffer: return kill_buffer_history_;
  258. case Mode::ISearch: return isearch_history_;
  259. default: return command_history_;
  260. }
  261. }
  262. void TuiEditor::add_to_history(const std::string& entry) {
  263. if (entry.empty()) return;
  264. auto& history = get_current_history();
  265. // Remove if already exists (move to front)
  266. auto it = std::find(history.begin(), history.end(), entry);
  267. if (it != history.end()) {
  268. history.erase(it);
  269. }
  270. // Add to front
  271. history.insert(history.begin(), entry);
  272. // Limit history size
  273. const size_t MAX_HISTORY = 100;
  274. if (history.size() > MAX_HISTORY) {
  275. history.resize(MAX_HISTORY);
  276. }
  277. }
  278. void TuiEditor::previous_history() {
  279. auto& history = get_current_history();
  280. if (history.empty()) return;
  281. if (history_index_ < history.size()) {
  282. command_buffer_ = history[history_index_];
  283. history_index_++;
  284. }
  285. }
  286. void TuiEditor::next_history() {
  287. auto& history = get_current_history();
  288. if (history.empty() || history_index_ == 0) return;
  289. history_index_--;
  290. if (history_index_ == 0) {
  291. command_buffer_ = "";
  292. } else {
  293. command_buffer_ = history[history_index_ - 1];
  294. }
  295. }
  296. void TuiEditor::reset_history_navigation() {
  297. history_index_ = 0;
  298. }
  299. void TuiEditor::update_completion_candidates(const std::string& prefix) {
  300. std::vector<std::string> candidates;
  301. if (mode_ == Mode::Command) {
  302. // Get command names from Lua
  303. auto& lua = core_->lua_api()->state();
  304. sol::function get_names = lua["get_command_names"];
  305. if (get_names.valid()) {
  306. candidates = get_names.call<std::vector<std::string>>();
  307. }
  308. } else {
  309. // Default to buffer names for BufferSwitch/KillBuffer
  310. candidates = core_->get_buffer_names();
  311. }
  312. completion_candidates_.clear();
  313. if (prefix.empty()) {
  314. completion_candidates_ = candidates;
  315. } else {
  316. for (const auto& name : candidates) {
  317. if (name.size() >= prefix.size() &&
  318. name.substr(0, prefix.size()) == prefix) {
  319. completion_candidates_.push_back(name);
  320. }
  321. }
  322. }
  323. completion_index_ = 0;
  324. }
  325. // Helper to reset completion state
  326. void TuiEditor::reset_completion() {
  327. completion_candidates_.clear();
  328. completion_index_ = 0;
  329. completion_prefix_.clear();
  330. }
  331. void TuiEditor::perform_search(bool find_next) {
  332. if (isearch_query_.empty()) {
  333. isearch_match_ = std::nullopt;
  334. isearch_failed_ = false;
  335. return;
  336. }
  337. Position start_search = core_->cursor();
  338. if (find_next) {
  339. if (isearch_forward_) {
  340. // Forward: move cursor forward 1 char to find next
  341. if (start_search.column < core_->buffer().line(start_search.line).size()) {
  342. start_search.column++;
  343. } else if (start_search.line < core_->buffer().line_count() - 1) {
  344. start_search.line++;
  345. start_search.column = 0;
  346. }
  347. } else {
  348. // Backward: move cursor backward 1 char
  349. if (start_search.column > 0) {
  350. start_search.column--;
  351. } else if (start_search.line > 0) {
  352. start_search.line--;
  353. start_search.column = core_->buffer().line(start_search.line).size();
  354. }
  355. }
  356. }
  357. std::optional<Range> result;
  358. if (isearch_forward_) {
  359. result = core_->buffer().find(isearch_query_, start_search);
  360. } else {
  361. result = core_->buffer().find_backward(isearch_query_, start_search);
  362. }
  363. if (result) {
  364. isearch_match_ = result;
  365. isearch_failed_ = false;
  366. core_->set_cursor(result->start);
  367. core_->adjust_scroll();
  368. } else {
  369. isearch_failed_ = true;
  370. }
  371. }
  372. bool TuiEditor::handle_input(int ch) {
  373. // Handle confirmation mode
  374. if (mode_ == Mode::ConfirmKill) {
  375. if (ch == 'y' || ch == 'Y') {
  376. if (core_->close_buffer(command_buffer_)) {
  377. message_line_ = "Closed modified buffer: " + command_buffer_;
  378. } else {
  379. message_line_ = "Failed to close buffer";
  380. }
  381. mode_ = Mode::Normal;
  382. command_buffer_.clear();
  383. reset_completion();
  384. } else if (ch == 'n' || ch == 'N' || ch == 27) { // n or ESC
  385. mode_ = Mode::Normal;
  386. message_line_ = "Cancelled kill buffer";
  387. command_buffer_.clear();
  388. reset_completion();
  389. }
  390. return true;
  391. }
  392. // Handle ISearch
  393. if (mode_ == Mode::ISearch) {
  394. // C-g (7) or ESC (27) -> Cancel
  395. if (ch == 27 || ch == 7) {
  396. core_->set_cursor(isearch_start_pos_);
  397. mode_ = Mode::Normal;
  398. message_line_ = "Quit";
  399. return true;
  400. }
  401. // RET -> Accept
  402. if (ch == '\n' || ch == '\r') {
  403. mode_ = Mode::Normal;
  404. message_line_ = "Mark saved";
  405. core_->buffer().set_mark(isearch_start_pos_);
  406. return true;
  407. }
  408. // C-s (19) -> Next
  409. if (ch == 19) {
  410. isearch_forward_ = true;
  411. perform_search(true);
  412. return true;
  413. }
  414. // C-r (18) -> Prev
  415. if (ch == 18) {
  416. isearch_forward_ = false;
  417. perform_search(true);
  418. return true;
  419. }
  420. // Backspace
  421. if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
  422. if (!isearch_query_.empty()) {
  423. isearch_query_.pop_back();
  424. perform_search(false);
  425. }
  426. return true;
  427. }
  428. // Printable
  429. if (ch >= 32 && ch <= 126) {
  430. isearch_query_ += static_cast<char>(ch);
  431. perform_search(false);
  432. return true;
  433. }
  434. // Other keys -> Exit and process
  435. mode_ = Mode::Normal;
  436. return handle_input(ch);
  437. }
  438. // Handle minibuffer/command mode
  439. if (mode_ == Mode::Command || mode_ == Mode::FindFile ||
  440. mode_ == Mode::BufferSwitch || mode_ == Mode::KillBuffer) {
  441. // ESC - cancel
  442. if (ch == 27) {
  443. mode_ = Mode::Normal;
  444. command_buffer_.clear();
  445. reset_completion();
  446. message_line_ = "Cancelled";
  447. return true;
  448. }
  449. // TAB - completion
  450. if (ch == '\t' && (mode_ == Mode::BufferSwitch || mode_ == Mode::KillBuffer || mode_ == Mode::Command)) {
  451. if (completion_candidates_.empty()) {
  452. // First TAB: save prefix and get candidates
  453. completion_prefix_ = command_buffer_;
  454. update_completion_candidates(completion_prefix_);
  455. if (!completion_candidates_.empty()) {
  456. command_buffer_ = completion_candidates_[0];
  457. completion_index_ = 0;
  458. } else {
  459. message_line_ = "No matches";
  460. }
  461. } else {
  462. // Cycle through candidates
  463. completion_index_ = (completion_index_ + 1) % completion_candidates_.size();
  464. command_buffer_ = completion_candidates_[completion_index_];
  465. }
  466. return true;
  467. }
  468. // Return - execute
  469. if (ch == '\n' || ch == '\r') {
  470. // Add to history before execution
  471. add_to_history(command_buffer_);
  472. if (mode_ == Mode::Command) {
  473. execute_command(command_buffer_);
  474. } else if (mode_ == Mode::FindFile) {
  475. if (core_->load_file(command_buffer_)) {
  476. message_line_ = "Loaded: " + command_buffer_;
  477. core_->lua_api()->execute("auto_activate_major_mode()") ;
  478. } else {
  479. message_line_ = "Failed to load: " + command_buffer_;
  480. }
  481. } else if (mode_ == Mode::BufferSwitch) {
  482. if (core_->switch_buffer_in_window(command_buffer_)) {
  483. message_line_ = "Switched to: " + command_buffer_;
  484. core_->lua_api()->execute("auto_activate_major_mode()") ;
  485. } else {
  486. message_line_ = "Buffer not found: " + command_buffer_;
  487. }
  488. } else if (mode_ == Mode::KillBuffer) {
  489. // Check for modification
  490. auto buf = core_->get_buffer_by_name(command_buffer_);
  491. if (buf && buf->is_modified()) {
  492. mode_ = Mode::ConfirmKill;
  493. message_line_ = "Buffer modified! Kill anyway? (y/n)";
  494. return true;
  495. }
  496. if (core_->close_buffer(command_buffer_)) {
  497. message_line_ = "Closed buffer: " + command_buffer_;
  498. } else {
  499. message_line_ = "Failed to close buffer: " + command_buffer_;
  500. }
  501. }
  502. mode_ = Mode::Normal;
  503. command_buffer_.clear();
  504. reset_completion();
  505. reset_history_navigation();
  506. return true;
  507. }
  508. // M-p (Alt+p) - Previous history
  509. if (ch == '\x90') { // Meta+p
  510. previous_history();
  511. reset_completion();
  512. return true;
  513. }
  514. // M-n (Alt+n) - Next history
  515. if (ch == '\x8E') { // Meta+n
  516. next_history();
  517. reset_completion();
  518. return true;
  519. }
  520. // Backspace
  521. if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
  522. if (!command_buffer_.empty()) {
  523. command_buffer_.pop_back();
  524. reset_completion(); // Reset completion on edit
  525. reset_history_navigation(); // Reset history on edit
  526. } else {
  527. mode_ = Mode::Normal;
  528. }
  529. return true;
  530. }
  531. // Printable characters
  532. if (ch >= 32 && ch <= 126) {
  533. command_buffer_ += static_cast<char>(ch);
  534. reset_completion(); // Reset completion on new input
  535. return true;
  536. }
  537. return true;
  538. }
  539. // Check for expired meta key
  540. if (waiting_for_meta_) {
  541. auto now = std::chrono::steady_clock::now();
  542. if (now - meta_time_ > META_TIMEOUT) {
  543. debug_log << "Meta timeout, treating ESC as Escape key" << std::endl;
  544. waiting_for_meta_ = false;
  545. // Process the ESC as a normal Escape key
  546. std::string final_key = "Escape";
  547. if (core_->lua_api()->execute_key_binding(final_key)) {
  548. core_->record_key_sequence(final_key);
  549. return true;
  550. }
  551. message_line_ = "Key: " + final_key;
  552. return false;
  553. }
  554. }
  555. // If we're waiting for a meta key and got one, combine them
  556. if (waiting_for_meta_) {
  557. waiting_for_meta_ = false;
  558. std::string base_key = resolve_key(ch);
  559. if (base_key.empty()) {
  560. debug_log << "Empty base key after ESC, ignoring" << std::endl;
  561. return false;
  562. }
  563. std::string key_name = "M-" + base_key;
  564. debug_log << "Meta sequence complete: " << key_name << std::endl;
  565. // Continue processing this meta key below
  566. return process_key(key_name);
  567. }
  568. // Check if this is the start of a meta sequence
  569. if (ch == 27) { // ESC
  570. waiting_for_meta_ = true;
  571. meta_time_ = std::chrono::steady_clock::now();
  572. debug_log << "ESC received, waiting for meta key..." << std::endl;
  573. return true;
  574. }
  575. // Normal mode - resolve key and try bindings
  576. std::string key_name = resolve_key(ch);
  577. if (key_name.empty()) {
  578. debug_log << "Empty key name, ignoring input" << std::endl;
  579. return false;
  580. }
  581. return process_key(key_name);
  582. }
  583. bool TuiEditor::process_key(const std::string& key_name) {
  584. debug_log << "Processing key: " << key_name << std::endl;
  585. // Use the new keybinding system
  586. KeyResult result = core_->lua_api()->process_key(key_name);
  587. switch (result) {
  588. case KeyResult::Executed:
  589. debug_log << "Key binding executed successfully" << std::endl;
  590. message_line_.clear(); // Clear any partial sequence display
  591. // Record the key if we're recording a macro (but not if it's F3/F4 themselves)
  592. if (key_name != "F3" && key_name != "F4") {
  593. core_->record_key_sequence(key_name);
  594. }
  595. return true;
  596. case KeyResult::Failed:
  597. debug_log << "Key binding execution failed" << std::endl;
  598. message_line_ = "Command failed";
  599. return true; // Key was handled, even though it failed
  600. case KeyResult::Partial:
  601. // Building a multi-key sequence
  602. message_line_ = core_->keybinding_manager().current_sequence_display();
  603. debug_log << "Partial sequence: " << message_line_ << std::endl;
  604. return true;
  605. case KeyResult::Timeout:
  606. debug_log << "Key sequence timed out" << std::endl;
  607. message_line_ = "Sequence timeout";
  608. // Fall through to try fallback bindings
  609. break;
  610. case KeyResult::Unbound:
  611. debug_log << "No key binding found, trying C++ fallbacks" << std::endl;
  612. // Fall through to C++ fallback bindings
  613. break;
  614. }
  615. // Clear any sequence display since we're not in a partial state
  616. message_line_.clear();
  617. // C++ fallback bindings - these should eventually be moved to Lua
  618. // Quit
  619. if (key_name == "C-q") {
  620. core_->request_quit();
  621. return true;
  622. }
  623. // Command mode
  624. if (key_name == "M-x") {
  625. mode_ = Mode::Command;
  626. command_buffer_.clear();
  627. return true;
  628. }
  629. // Navigation fallbacks (these should be in Lua)
  630. if (key_name == "ArrowUp") {
  631. core_->move_up();
  632. return true;
  633. }
  634. if (key_name == "ArrowDown") {
  635. core_->move_down();
  636. return true;
  637. }
  638. if (key_name == "ArrowLeft") {
  639. core_->move_left();
  640. return true;
  641. }
  642. if (key_name == "ArrowRight") {
  643. core_->move_right();
  644. return true;
  645. }
  646. if (key_name == "Home") { core_->move_to_line_start(); return true; }
  647. if (key_name == "End") { core_->move_to_line_end(); return true; }
  648. // Editing fallbacks (these should also be in Lua)
  649. if (key_name == "Backspace") {
  650. auto cursor = core_->cursor();
  651. core_->buffer().erase_char(cursor);
  652. if (cursor.column > 0) {
  653. core_->set_cursor({cursor.line, cursor.column - 1});
  654. } else if (cursor.line > 0) {
  655. size_t prev_line_len = core_->buffer().line(cursor.line - 1).size();
  656. core_->set_cursor({cursor.line - 1, prev_line_len});
  657. }
  658. return true;
  659. }
  660. if (key_name == "Delete") {
  661. auto cursor = core_->cursor();
  662. if (cursor.column < core_->buffer().line(cursor.line).size()) {
  663. core_->buffer().erase_char({cursor.line, cursor.column + 1});
  664. } else if (cursor.line < core_->buffer().line_count() - 1) {
  665. core_->buffer().erase_char({cursor.line + 1, 0});
  666. }
  667. return true;
  668. }
  669. if (key_name == "Return") {
  670. auto cursor = core_->cursor();
  671. core_->buffer().insert_newline(cursor);
  672. core_->set_cursor({cursor.line + 1, 0});
  673. return true;
  674. }
  675. if (key_name == "Tab") {
  676. auto cursor = core_->cursor();
  677. core_->buffer().insert(cursor, " ");
  678. core_->set_cursor({cursor.line, cursor.column + 4});
  679. return true;
  680. }
  681. // Insert printable characters (use original key_name for this)
  682. if (key_name.size() == 1 && key_name[0] >= 32 && key_name[0] <= 126) {
  683. auto cursor = core_->cursor();
  684. core_->buffer().insert_char(cursor, key_name[0]);
  685. core_->set_cursor({cursor.line, cursor.column + 1});
  686. return true;
  687. }
  688. return false;
  689. }
  690. void TuiEditor::execute_command(const std::string& cmd) {
  691. if (cmd.empty()) return;
  692. std::istringstream iss(cmd);
  693. std::string command;
  694. iss >> command;
  695. if (command == "q" || command == "quit") {
  696. core_->request_quit();
  697. return;
  698. }
  699. if (command == "w" || command == "write") {
  700. core_->buffer().save();
  701. message_line_ = "Saved";
  702. return;
  703. }
  704. if (command == "wq") {
  705. core_->buffer().save();
  706. core_->request_quit();
  707. return;
  708. }
  709. if (command == "e" || command == "edit") {
  710. std::string path;
  711. std::getline(iss >> std::ws, path);
  712. if (!path.empty()) {
  713. if (core_->load_file(path)) {
  714. message_line_ = "Loaded: " + path;
  715. core_->lua_api()->execute("auto_activate_major_mode()") ;
  716. } else { message_line_ = "Failed to load: " + path;
  717. }
  718. } else {
  719. message_line_ = "Usage: :e <filename>";
  720. }
  721. return;
  722. }
  723. // Try executing via command registry first
  724. auto& lua = core_->lua_api()->state();
  725. sol::function exec_cmd = lua["execute_extended_command"];
  726. if (exec_cmd.valid()) {
  727. bool result = exec_cmd(cmd);
  728. if (result) {
  729. return;
  730. }
  731. }
  732. // Fallback: Try executing as Lua code
  733. if (core_->lua_api()->execute(cmd)) {
  734. message_line_ = "Lua executed";
  735. } else {
  736. message_line_ = "Unknown command: " + cmd;
  737. }
  738. }
  739. int TuiEditor::get_attributes_for_face(const std::string& face_name) {
  740. auto theme = core_->active_theme();
  741. if (!theme) return 0;
  742. return theme->get_face_attributes_ncurses(face_name);
  743. }
  744. void TuiEditor::render() {
  745. // Clear and update screen info
  746. getmaxyx(stdscr, height_, width_);
  747. // Set background color from theme
  748. auto theme = core_->active_theme();
  749. if (theme) {
  750. int bg_color_pair = theme->get_color_pair(ThemeElement::Background);
  751. bkgd(bg_color_pair);
  752. }
  753. clear();
  754. // Calculate content area (leave room for message line only)
  755. int content_height = height_ - 1;
  756. int content_width = width_;
  757. // Render the layout tree recursively (now includes per-window modelines)
  758. render_layout_node(core_->root_layout(), 0, 0, content_width, content_height);
  759. // Global message/command line (last line)
  760. render_message_line();
  761. // Refresh screen
  762. refresh();
  763. }
  764. void TuiEditor::render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height) {
  765. if (!node) return;
  766. if (node->type == LayoutNode::Type::Leaf) {
  767. // Render a single window
  768. render_window(node->window, x, y, width, height);
  769. } else if (node->type == LayoutNode::Type::HorizontalSplit) {
  770. // Split horizontally: top and bottom windows
  771. int top_height = height / 2;
  772. int bottom_height = height - top_height;
  773. render_layout_node(node->child1, x, y, width, top_height);
  774. render_layout_node(node->child2, x, y + top_height, width, bottom_height);
  775. } else if (node->type == LayoutNode::Type::VerticalSplit) {
  776. // Split vertically: left and right windows
  777. int separator_width = (width > 2) ? 1 : 0;
  778. int available_width = width - separator_width;
  779. int left_width = available_width / 2;
  780. int right_width = available_width - left_width;
  781. render_layout_node(node->child1, x, y, left_width, height);
  782. // Draw separator if enabled
  783. if (separator_width > 0) {
  784. int attrs = get_attributes_for_face("window-divider");
  785. attron(attrs);
  786. int sep_x = x + left_width;
  787. for (int i = 0; i < height; ++i) {
  788. mvaddch(y + i, sep_x, ACS_VLINE);
  789. }
  790. attroff(attrs);
  791. }
  792. render_layout_node(node->child2, x + left_width + separator_width, y, right_width, height);
  793. }
  794. }
  795. void TuiEditor::render_window(std::shared_ptr<Window> window, int x, int y, int width, int height) {
  796. if (!window) return;
  797. // Check configuration for line numbers and modeline
  798. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  799. bool show_modeline = core_->config().get<bool>("show_modeline", true);
  800. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  801. int modeline_height = show_modeline ? 1 : 0;
  802. // Update window viewport size (reserve space for modeline)
  803. int content_width = width - line_number_width;
  804. int content_height = height - modeline_height;
  805. window->set_viewport_size(content_width, content_height);
  806. // Get window data
  807. const auto& buffer = window->buffer();
  808. const auto cursor = window->cursor();
  809. auto [start_line, end_line] = window->visible_line_range();
  810. bool is_active = (window == core_->active_window());
  811. debug_log << "Render window at " << x << "," << y << " size " << width << "x" << height
  812. << " viewport=" << start_line << "-" << end_line
  813. << " cursor=(" << cursor.line << "," << cursor.column << ")"
  814. << " active=" << is_active << std::endl;
  815. // Render buffer lines
  816. for (int screen_y = 0; screen_y < content_height && start_line + screen_y < end_line; ++screen_y) {
  817. size_t buffer_line_idx = start_line + screen_y;
  818. const auto& line_text = buffer.line(buffer_line_idx);
  819. // Clear this line
  820. move(y + screen_y, x);
  821. for (int i = 0; i < width; ++i) addch(' ');
  822. // Line number (if enabled)
  823. if (show_line_numbers) {
  824. mvprintw(y + screen_y, x, "%3zu ", buffer_line_idx + 1);
  825. }
  826. // Line content with syntax highlighting
  827. if (!line_text.empty()) {
  828. int max_content_width = content_width - 1;
  829. const auto& styles = buffer.get_line_styles(buffer_line_idx);
  830. if (styles.empty()) {
  831. // No styles, render as plain text
  832. std::string display_text = line_text;
  833. if ((int)display_text.length() > max_content_width) {
  834. display_text = display_text.substr(0, max_content_width - 3) + "...";
  835. }
  836. mvprintw(y + screen_y, x + line_number_width, "%s", display_text.c_str());
  837. } else {
  838. // Render with syntax highlighting
  839. size_t pos = 0;
  840. int screen_x = x + line_number_width;
  841. for (const auto& styled : styles) {
  842. size_t start = styled.range.start.column;
  843. size_t end = std::min(styled.range.end.column, line_text.size());
  844. // Render unstyled text before this styled range
  845. if (pos < start && pos < line_text.size()) {
  846. size_t len = std::min(start - pos, line_text.size() - pos);
  847. std::string unstyled = line_text.substr(pos, len);
  848. mvprintw(y + screen_y, screen_x, "%s", unstyled.c_str());
  849. screen_x += unstyled.length();
  850. pos = start;
  851. }
  852. // Render styled text
  853. if (pos < end && pos < line_text.size()) {
  854. size_t len = std::min(end - pos, line_text.size() - pos);
  855. std::string styled_text = line_text.substr(pos, len);
  856. // Apply color
  857. int attrs = get_attributes_for_face(styled.attr.face_name);
  858. attron(attrs);
  859. mvprintw(y + screen_y, screen_x, "%s", styled_text.c_str());
  860. attroff(attrs);
  861. screen_x += styled_text.length();
  862. pos = end;
  863. }
  864. }
  865. // Render remaining unstyled text
  866. if (pos < line_text.size()) {
  867. std::string remaining = line_text.substr(pos);
  868. if ((int)(screen_x - x - line_number_width + remaining.length()) > max_content_width) {
  869. remaining = remaining.substr(0, max_content_width - (screen_x - x - line_number_width) - 3) + "...";
  870. }
  871. mvprintw(y + screen_y, screen_x, "%s", remaining.c_str());
  872. }
  873. }
  874. }
  875. // Highlight ISearch match
  876. if (mode_ == Mode::ISearch && isearch_match_ && isearch_match_->start.line == buffer_line_idx) {
  877. size_t match_start = isearch_match_->start.column;
  878. size_t match_len = isearch_match_->end.column - match_start;
  879. if (match_start < line_text.size()) {
  880. size_t display_len = std::min(match_len, line_text.size() - match_start);
  881. std::string matched_text = line_text.substr(match_start, display_len);
  882. int match_x = x + line_number_width + match_start;
  883. // Simple clipping check
  884. if (match_x < x + width) {
  885. int attrs = get_attributes_for_face(isearch_failed_ ? "isearch-fail" : "isearch");
  886. if (attrs == 0) attrs = A_REVERSE; // Fallback
  887. attron(attrs);
  888. mvprintw(y + screen_y, match_x, "%s", matched_text.c_str());
  889. attroff(attrs);
  890. }
  891. }
  892. }
  893. // Show cursor if this is the cursor line and this is the active window
  894. if (buffer_line_idx == cursor.line && is_active && mode_ == Mode::Normal) {
  895. int cursor_screen_x = x + line_number_width + cursor.column;
  896. if (cursor_screen_x < x + width) {
  897. char cursor_char = ' ';
  898. if (cursor.column < line_text.size()) {
  899. cursor_char = line_text[cursor.column];
  900. }
  901. int attrs = get_attributes_for_face("cursor");
  902. if (attrs == 0) attrs = A_REVERSE;
  903. attron(attrs);
  904. mvaddch(y + screen_y, cursor_screen_x, cursor_char);
  905. attroff(attrs);
  906. }
  907. }
  908. }
  909. // Fill remaining lines (for empty lines below buffer) - no tildes
  910. size_t displayed_lines = std::min((size_t)content_height, end_line - start_line);
  911. for (int screen_y = displayed_lines; screen_y < content_height; ++screen_y) {
  912. move(y + screen_y, x);
  913. for (int i = 0; i < width; ++i) addch(' ');
  914. }
  915. // Render modeline for this window
  916. if (show_modeline) {
  917. render_window_modeline(window, x, y + content_height, width, is_active);
  918. }
  919. }
  920. void TuiEditor::render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active) {
  921. const auto& buffer = window->buffer();
  922. const auto cursor = window->cursor();
  923. // Choose modeline colors
  924. std::string face_name = is_active ? "mode-line" : "mode-line-inactive";
  925. int attrs = get_attributes_for_face(face_name);
  926. if (attrs == 0) attrs = is_active ? A_REVERSE : A_DIM;
  927. attron(attrs);
  928. // Clear the modeline
  929. move(y, x);
  930. for (int i = 0; i < width; ++i) addch(' ');
  931. // Create modeline content
  932. std::string modeline;
  933. // Buffer name and modification status
  934. modeline += buffer.name();
  935. if (buffer.is_modified()) modeline += " [+] ";
  936. // Cursor position
  937. modeline += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
  938. // Major mode (if available)
  939. // TODO: Add major mode support when available
  940. // Right-aligned content (percentage through file)
  941. std::string right_side;
  942. if (buffer.line_count() > 0) {
  943. int percentage = (cursor.line * 100) / (buffer.line_count() - 1);
  944. right_side = " " + std::to_string(percentage) + "%";
  945. }
  946. // Truncate modeline if too long
  947. int available_width = width - right_side.length();
  948. if ((int)modeline.length() > available_width) {
  949. modeline = modeline.substr(0, available_width - 3) + "...";
  950. }
  951. // Render left side
  952. mvprintw(y, x, "%s", modeline.c_str());
  953. // Render right side
  954. if (!right_side.empty()) {
  955. mvprintw(y, x + width - right_side.length(), "%s", right_side.c_str());
  956. }
  957. // Turn off modeline attributes
  958. attroff(attrs);
  959. }
  960. void TuiEditor::render_status_line() {
  961. const auto cursor = core_->cursor();
  962. const auto& buffer = core_->buffer();
  963. int status_y = height_ - 2;
  964. int attrs = get_attributes_for_face("mode-line");
  965. if (attrs == 0) attrs = A_REVERSE;
  966. attron(attrs);
  967. move(status_y, 0);
  968. clrtoeol();
  969. std::string status = buffer.name();
  970. if (buffer.is_modified()) status += " [+] ";
  971. status += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
  972. status += " | " + std::to_string(width_) + "x" + std::to_string(height_);
  973. if (mode_ == Mode::Command) status += " [CMD]";
  974. else if (mode_ == Mode::FindFile) status += " [FILE]";
  975. else if (mode_ == Mode::BufferSwitch) status += " [BUFFER]";
  976. else if (mode_ == Mode::KillBuffer) status += " [KILL]";
  977. else if (mode_ == Mode::ConfirmKill) status += " [CONFIRM]";
  978. else if (mode_ == Mode::ISearch) status += " [I-SEARCH]";
  979. mvprintw(status_y, 0, "%s", status.c_str());
  980. attroff(attrs);
  981. }
  982. void TuiEditor::render_message_line() {
  983. int msg_y = height_ - 1;
  984. int attrs = get_attributes_for_face("minibuffer-prompt");
  985. attron(attrs);
  986. move(msg_y, 0);
  987. clrtoeol();
  988. if (mode_ == Mode::Command) {
  989. mvprintw(msg_y, 0, ":%s", command_buffer_.c_str());
  990. } else if (mode_ == Mode::FindFile) {
  991. mvprintw(msg_y, 0, "Find file: %s", command_buffer_.c_str());
  992. } else if (mode_ == Mode::BufferSwitch) {
  993. std::string prompt = "Switch to buffer: " + command_buffer_;
  994. if (!completion_candidates_.empty()) {
  995. prompt += " [" + std::to_string(completion_index_ + 1) + "/" +
  996. std::to_string(completion_candidates_.size()) + "]";
  997. }
  998. mvprintw(msg_y, 0, "%s", prompt.c_str());
  999. } else if (mode_ == Mode::KillBuffer) {
  1000. std::string prompt = "Kill buffer: " + command_buffer_;
  1001. if (!completion_candidates_.empty()) {
  1002. prompt += " [" + std::to_string(completion_index_ + 1) + "/" +
  1003. std::to_string(completion_candidates_.size()) + "]";
  1004. }
  1005. mvprintw(msg_y, 0, "%s", prompt.c_str());
  1006. } else if (mode_ == Mode::ISearch) {
  1007. std::string prompt = (isearch_failed_ ? "Failing " : "") + std::string("I-search: ") + isearch_query_;
  1008. mvprintw(msg_y, 0, "%s", prompt.c_str());
  1009. } else if (!message_line_.empty()) {
  1010. mvprintw(msg_y, 0, "%s", message_line_.c_str());
  1011. }
  1012. attroff(attrs);
  1013. }
  1014. namespace lumacs {
  1015. std::unique_ptr<IEditorView> create_tui_editor() {
  1016. return std::make_unique<TuiEditor>();
  1017. }
  1018. }