Преглед изворни кода

feat: add support for curses based interactive prompts (move up/down in history)

Sam Jaffe пре 1 година
родитељ
комит
76fc90f92a
5 измењених фајлова са 166 додато и 17 уклоњено
  1. 4 0
      cli.xcodeproj/project.pbxproj
  2. 8 3
      include/cli/cli.h
  3. 49 0
      include/cli/tape.h
  4. 23 14
      src/cli.cxx
  5. 82 0
      src/tape.cxx

+ 4 - 0
cli.xcodeproj/project.pbxproj

@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		CD6CF58C2C4FF0D100CDF5E3 /* tape.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD6CF58A2C4FF0D100CDF5E3 /* tape.cxx */; };
 		CDC748C725313592008D9D1D /* libcli.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CDC748A52531353C008D9D1D /* libcli.a */; };
 		CDC748E5253136FD008D9D1D /* cli.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDC748E2253136FD008D9D1D /* cli.cxx */; };
 		CDC748E725313CBC008D9D1D /* cli_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDC748E625313CBC008D9D1D /* cli_test.cxx */; };
@@ -80,6 +81,7 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		CD6CF58A2C4FF0D100CDF5E3 /* tape.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tape.cxx; sourceTree = "<group>"; };
 		CD7B8B7825E2946900867188 /* xcode_gtest_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = xcode_gtest_helper.h; sourceTree = "<group>"; };
 		CD87CDAE29BE022900C5949D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
 		CDC748A52531353C008D9D1D /* libcli.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libcli.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -159,6 +161,7 @@
 			isa = PBXGroup;
 			children = (
 				CDC748E2253136FD008D9D1D /* cli.cxx */,
+				CD6CF58A2C4FF0D100CDF5E3 /* tape.cxx */,
 			);
 			path = src;
 			sourceTree = "<group>";
@@ -378,6 +381,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				CDC748E5253136FD008D9D1D /* cli.cxx in Sources */,
+				CD6CF58C2C4FF0D100CDF5E3 /* tape.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 8 - 3
include/cli/cli.h

@@ -16,6 +16,7 @@
 #include <vector>
 
 #include <string_utils/cast.h>
+#include <string_utils/tokenizer.h>
 
 #include "lambda_cast.h"
 #include "types.h"
@@ -28,11 +29,12 @@ public:
   using callback = std::function<void(args_t const &)>;
 
 private:
-  std::istream &in_;
+  string_utils::EscapedTokenizer tokenize_{" ", {'"', "\\\""}};
+  std::istream *in_;
   std::string prompt_;
   std::unordered_map<std::string, size_t> arguments_;
   std::unordered_map<std::string, callback> callbacks_;
-  bool eof_;
+  mutable bool eof_;
 
 public:
   explicit cli(std::string const & prompt, std::istream &in = std::cin);
@@ -52,9 +54,12 @@ public:
     return register_query(handle, lambdas::FFL(cb));
   }
   
-  void run() const;
+  void run() const { in_ ? run_stream() : run_interactive(); }
   
 private:
+  void run_interactive() const;
+  void run_stream() const;
+  void run(std::string &command) const;
   bool active() const;
   
   cli(cli const &) = delete;

+ 49 - 0
include/cli/tape.h

@@ -0,0 +1,49 @@
+//
+//  tape.hpp
+//  cli
+//
+//  Created by Sam Jaffe on 7/23/24.
+//  Copyright © 2024 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace cli {
+struct Tape {
+public:
+  using callback_type = std::function<void(std::string)>;
+  using value_type = std::string;
+  using char_type = char;
+  
+private:
+  size_t negative_offset_{0};
+  size_t cursor_offset_{0};
+  std::vector<value_type> elements_{""};
+  callback_type callback_;
+
+public:
+  explicit Tape(callback_type const &callback) : Tape(1, callback) {}
+  Tape(size_t n, callback_type const &callback);
+    
+  value_type const &get() const;
+  
+  void readch();
+  
+private:
+  value_type &get();
+  
+  void down();
+  void up();
+  void left();
+  void right();
+  
+  void insert(char_type value);
+  void erase();
+  void clear();
+};
+
+}

+ 23 - 14
src/cli.cxx

@@ -7,13 +7,12 @@
 //
 
 #include "cli/cli.h"
-
-#include <string_utils/tokenizer.h>
+#include "cli/tape.h"
 
 namespace cli {
 
 cli::cli(std::string const & prompt, std::istream &in)
-    : in_(in), prompt_(prompt), eof_(false) {
+    : in_(&in), prompt_(prompt), eof_(false) {
   register_callback("quit", [this] { eof_ = true; });
   register_callback("help", [this] {
     for (auto & [k, v] : arguments_) {
@@ -32,18 +31,28 @@ bool cli::active() const {
   return !eof_ && (std::cout << prompt_).good();
 }
 
-void cli::run() const {
+void cli::run(std::string &command) const {
+  auto views = tokenize_(command);
+  command = views.front();
+  views.erase(views.begin());
+  if (!callbacks_.count(command)) {
+    std::cerr << "Unknown command: " << command << "\n";
+    return;
+  }
+  callbacks_.at(command)(views);
+}
+
+void cli::run_interactive() const {
+  Tape tape(128, [this](std::string line) { run(line); });
+  while (active()) {
+    tape.readch();
+  }
+}
+
+void cli::run_stream() const {
   std::string command;
-  while (active() && std::getline(in_, command)) {
-    auto split = string_utils::EscapedTokenizer(" ", {'"', "\\\""});
-    auto views = split(command);
-    command = views.front();
-    views.erase(views.begin());
-    if (!callbacks_.count(command)) {
-      std::cerr << "Unknown command: " << command << "\n";
-      continue;
-    }
-    callbacks_.at(command)(views);
+  while (active() && std::getline(*in_, command)) {
+    run(command);
   }
 }
 

+ 82 - 0
src/tape.cxx

@@ -0,0 +1,82 @@
+//
+//  tape.cxx
+//  cli
+//
+//  Created by Sam Jaffe on 7/23/24.
+//  Copyright © 2024 Sam Jaffe. All rights reserved.
+//
+
+#include "cli/tape.h"
+
+#include <curses.h>
+
+namespace cli {
+Tape::Tape(size_t n, callback_type const &callback)
+  : callback_(callback) {
+  elements_.reserve(n);
+}
+  
+Tape::value_type &Tape::get() {
+  return *(elements_.rbegin() + negative_offset_);
+}
+
+Tape::value_type const &Tape::get() const {
+  return *(elements_.rbegin() + negative_offset_);
+}
+
+void Tape::down() {
+  if (negative_offset_ == 0) { return; }
+  --negative_offset_;
+  cursor_offset_ = 0;
+}
+
+void Tape::up() {
+  if (negative_offset_ >= elements_.size()) { return; }
+  ++negative_offset_;
+  cursor_offset_ = 0;
+}
+
+void Tape::left() {
+  if (cursor_offset_ >= get().size()) { return; }
+  ++cursor_offset_;
+}
+
+void Tape::right() {
+  if (cursor_offset_ == 0) { return; }
+  --cursor_offset_;
+}
+
+void Tape::insert(char_type value) {
+  get().insert(get().size() - cursor_offset_, 1, value);
+}
+
+void Tape::erase() {
+  if (get().empty()) { return; }
+  get().erase(get().end() - 1 - cursor_offset_);
+}
+
+void Tape::clear() {
+  negative_offset_ = 0;
+  cursor_offset_ = 0;
+  elements_.emplace_back();
+}
+
+void Tape::readch() {
+  int ch = getch();
+  if (ch == -1) {
+    return;
+  }
+  switch (ch) {
+  case KEY_UP: return up();
+  case KEY_DOWN: return down();
+  case KEY_LEFT: return left();
+  case KEY_RIGHT: return right();
+  case KEY_BACKSPACE: return erase();
+  case KEY_ENTER:
+    callback_(get());
+    return clear();
+  default:
+    return insert(static_cast<char>(ch));
+  }
+}
+}