Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions include/CliPrompt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <nds.h>

#include <functional>
#include <iostream>
#include <string>
#include <vector>
Expand All @@ -17,6 +18,10 @@ struct MyTickTask : TickTask
// - input line history with arrow-key & d-pad button navigation
class CliPrompt
{
public:
using AutocompleteCallback = std::function<void(const std::string &, std::vector<std::string> &)>;

private:
std::ostream *ostr = &std::cout;
std::string prompt = "> ";

Expand All @@ -40,6 +45,9 @@ class CliPrompt
// past the end of the line history.
std::string savedInput;

std::vector<std::string> autocompleteOptions;
AutocompleteCallback autocompleteCallback;

void resetKeypressState() { _enterPressed = _foldPressed = {}; }
void flashCursor();
void handleBackspace();
Expand All @@ -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();
Expand All @@ -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; }
Expand Down
3 changes: 3 additions & 0 deletions include/Shell.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> &options);

public:
const int console;

Expand Down
37 changes: 36 additions & 1 deletion src/CliPrompt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -129,14 +161,17 @@ 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:
case DVK_MENU:
case DVK_SHIFT:
break;

case DVK_TAB:
handleTab();
break;

case DVK_FOLD:
_foldPressed = true;
break;
Expand Down
6 changes: 6 additions & 0 deletions src/Commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
49 changes: 47 additions & 2 deletions src/Shell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::string> &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";
Expand Down