diff --git a/include/CliPrompt.hpp b/include/CliPrompt.hpp index 9de8b57..c5d0e55 100644 --- a/include/CliPrompt.hpp +++ b/include/CliPrompt.hpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -17,6 +18,10 @@ struct MyTickTask : TickTask // - input line history with arrow-key & d-pad button navigation class CliPrompt { +public: + using AutocompleteCallback = std::function &)>; + +private: std::ostream *ostr = &std::cout; std::string prompt = "> "; @@ -40,6 +45,9 @@ class CliPrompt // past the end of the line history. std::string savedInput; + std::vector autocompleteOptions; + AutocompleteCallback autocompleteCallback; + void resetKeypressState() { _enterPressed = _foldPressed = {}; } void flashCursor(); void handleBackspace(); @@ -48,6 +56,7 @@ class CliPrompt void handleRight(); void handleUp(); void handleDown(); + void handleTab(); // Returns whether an event was run via a keypad press. bool processKeypad(); @@ -71,8 +80,8 @@ class CliPrompt // Set the text to print before the cursor. void setPrompt(const std::string &s) { prompt = s; } - // Set the character to print as the cursor. - // void setCursor(char c) { cursor = c; } + // Set the autocomplete callback function. It is called with the current input when the user presses Tab. + void setAutocompleteCallback(const AutocompleteCallback &cb) { autocompleteCallback = cb; } // Read the input buffer. const std::string &getInput() const { return input; } diff --git a/include/Shell.hpp b/include/Shell.hpp index 9ae5d85..ad10ed4 100644 --- a/include/Shell.hpp +++ b/include/Shell.hpp @@ -49,6 +49,9 @@ class Shell // Does NOT free any resources that they previously pointed to! void ResetStreams(); + // Autocomplete callback for CliPrompt. + void AutocompleteCallback(const std::string &input, std::vector &options); + public: const int console; diff --git a/src/CliPrompt.cpp b/src/CliPrompt.cpp index cbc0ca7..2121038 100644 --- a/src/CliPrompt.cpp +++ b/src/CliPrompt.cpp @@ -96,6 +96,38 @@ void CliPrompt::handleDown() *ostr << "\r\e[2K" << prompt << input; } +void CliPrompt::handleTab() +{ + if (!autocompleteCallback) + return; + + autocompleteOptions.clear(); + autocompleteCallback(input, autocompleteOptions); + + if (autocompleteOptions.empty()) + return; + + if (autocompleteOptions.size() == 1) + { + // only 1 match: replace input with received string. + + // hack for appending an autocompleted "final token" (e.g. a filepath) + input.resize(input.find_last_of(' ') + 1); + input += autocompleteOptions[0]; + + *ostr << "\r\e[2K" << prompt << input; + return; + } + + *ostr << "\n\e[90m"; // print in gray, make customizable later + for (const auto &s : autocompleteOptions) + if (s.contains(' ')) + *ostr << '"' << s << "\" "; + else + *ostr << s << ' '; + *ostr << "\e[39m\n" << prompt << input; +} + bool CliPrompt::processKeypad() { #ifndef NDSH_THREADING @@ -129,7 +161,6 @@ void CliPrompt::processKeyboard() { case 0: // just in case case NOKEY: - case DVK_TAB: // TODO: use tabs for autocomplete case DVK_CTRL: case DVK_ALT: case DVK_CAPS: @@ -137,6 +168,10 @@ void CliPrompt::processKeyboard() case DVK_SHIFT: break; + case DVK_TAB: + handleTab(); + break; + case DVK_FOLD: _foldPressed = true; break; diff --git a/src/Commands.cpp b/src/Commands.cpp index c790422..916efad 100644 --- a/src/Commands.cpp +++ b/src/Commands.cpp @@ -47,6 +47,12 @@ void ls(const Context &ctx) return; } + if (!fs::is_directory(path)) + { + ctx.out << path << '\n'; + return; + } + for (const auto &entry : fs::directory_iterator{path}) { const auto filename = entry.path().filename().string(); diff --git a/src/Shell.cpp b/src/Shell.cpp index b17e94b..2774cb4 100644 --- a/src/Shell.cpp +++ b/src/Shell.cpp @@ -19,12 +19,15 @@ Shell::Shell(const int console) : ostr{Consoles::GetStream(console)}, console{console} { + SetEnv("PWD", "/"); prompt.setOutputStream(ostr); if (!fsInitialized()) return; prompt.setLineHistoryFromFile(".ndsh_history"); if (fs::exists(".ndshrc")) SourceFile(".ndshrc"); + prompt.setAutocompleteCallback( + std::bind(&Shell::AutocompleteCallback, this, std::placeholders::_1, std::placeholders::_2)); } Shell::~Shell() @@ -73,8 +76,9 @@ void Shell::ProcessLine(std::string_view line) return; // Trim leading and trailing whitespace - const auto first_non_whitespace = line.find_first_not_of(" \t\n\r"); - line = line.substr(first_non_whitespace, line.find_last_not_of(" \t\n\r") - first_non_whitespace + 1); + const auto first_non_whitespace = line.find_first_not_of(' '); + if (first_non_whitespace != std::string::npos) + line = line.substr(first_non_whitespace, line.find_last_not_of(' ') - first_non_whitespace + 1); if (line.empty()) return; @@ -246,6 +250,47 @@ void Shell::RedirectOutput(int fd, std::ostream &ostr) // in the future, a full file descriptor table??????? } +void Shell::AutocompleteCallback(const std::string &_input, std::vector &options) +{ + std::string_view input{_input}; + + // Trim ONLY leading whitespace (trailing matters here!) + const auto first_non_whitespace = input.find_first_not_of(' '); + if (first_non_whitespace != std::string::npos) + input = input.substr(input.find_first_not_of(' ')); + + if (!input.contains(' ')) + { + // autocomplete the command + + if (Commands::MAP.contains(std::string{input})) + // it's already a valid command + return; + + for (const auto &[cmdname, _] : Commands::MAP) + if (cmdname.starts_with(input)) + options.emplace_back(cmdname); + } + else if (fsInitialized()) + { + // a command is already there, autocomplete filenames. + // get to the last space-separated token, this will be our current filename + input = input.substr(input.find_last_of(' ') + 1); + + const auto &pwd = env["PWD"]; + + if (!fs::exists(pwd)) + return; + + for (const auto &entry : fs::directory_iterator{pwd}) + { + const auto filename{entry.path().filename().string()}; + if (filename.starts_with(input)) + options.emplace_back(filename); + } + } +} + void Shell::StartPrompt() { ostr << "\e[96mtrustytrojan/nds-shell\nstar me on github!!!\e[39m\n\nrun 'help' for help\n\n";