Skip to content

Commit

Permalink
feat: handle list (#3)
Browse files Browse the repository at this point in the history
Signed-off-by: Tony Gorez <[email protected]>
  • Loading branch information
tony-go authored Feb 27, 2024
1 parent 5e6cd2c commit c342e8a
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ example: build

# Run the tests
test: build
cd $(BUILD_DIR) && ctest
cd $(BUILD_DIR) && ctest --verbose

# Clean up build artifacts
clean:
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,40 @@
- 🌍 Cross-platform (work in progress).
- 🛠️ CMake-friendly integration (work in progress)

## 📖 Usage
## 📖 Usage (API)

### `std::optional<std::string> Prompt::ask_for_input(std::string question)`

```cpp
#include <qupp/prompt/prompt.h>

int main() {
qupp::prompt::Prompt prompt;

auto result = prompt.ask_for_input("What's your name? ");
std::cout << "Hello, " << result.value() << "!" << std::endl;

return 0;
}
```

### `std::optional<std::string> Prompt::ask_from_list(std::string question, std::vector<std::string> options)`

```cpp
#include <qupp/prompt/prompt.h>

int main() {
qupp::prompt::Prompt prompt;

auto selected = prompt.select_from_list(
"Select an option:", {"Option 1", "Option 2", "Option 3 "});
if (selected.has_value()) {
std::cout << "You selected: " << selected.value() << "\n";
} else {
std::cout << "You did not select anything."
<< "\n";
}

return 0;
}
```
Expand Down
17 changes: 13 additions & 4 deletions examples/simple_prompt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@

int main() {
qupp::prompt::Prompt prompt;
auto result = prompt.ask_for_input("This is a question:");

auto result = prompt.ask_for_input("What is your name?");
if (result.has_value()) {
std::cout << "You answered: " << result.value() << std::endl;
std::cout << "Hello " << result.value() << "\n";
} else {
std::cout << "You did not answer." << std::endl;
std::cout << "Hello, I did not get your name."
<< "\n";
}

return 0;
auto selected = prompt.select_from_list(
"Select an option:", {"Option 1", "Option 2", "Option 3 "});
if (selected.has_value()) {
std::cout << "You selected: " << selected.value() << "\n";
} else {
std::cout << "You did not select anything."
<< "\n";
}
}
29 changes: 26 additions & 3 deletions src/io/include/qupp/io/io.h
Original file line number Diff line number Diff line change
@@ -1,34 +1,57 @@
#pragma once

#include <string>
#include <termios.h>

namespace qupp::io {
struct TerminalInterface {
virtual ~TerminalInterface() = default;

virtual void print(const std::string &message, bool break_line) = 0;
virtual std::string read() = 0;
virtual void print_with_prefix(const std::string &prefix,
const std::string &message,
bool break_line) = 0;
virtual std::string read_from_console() = 0;
virtual int read_in_list(int list_size) = 0;
virtual void move_cursor_left(int n) = 0;
virtual void move_cursor_right(int n) = 0;
virtual void move_cursor_up(int n) = 0;
virtual void move_cursor_down(int n) = 0;
virtual void clear_line() = 0;
virtual void enable_raw_mode() = 0;
virtual void disable_raw_mode() = 0;
};

struct Terminal : public TerminalInterface {
Terminal() noexcept; // Declare the default constructor explicitly
~Terminal(); // Declare the destructor explicitly

static Terminal &get_instance();

void print(const std::string &message, bool break_line);
std::string read();
void print_with_prefix(const std::string &prefix, const std::string &message,
bool break_line);
std::string read_from_console();
int read_in_list(int list_size);
void move_cursor_left(int n);
void move_cursor_right(int n);
void move_cursor_up(int n);
void move_cursor_down(int n);
void clear_line();
void enable_raw_mode();
void disable_raw_mode();

private:
// This is a key part of the Singleton Pattern; it prevents the creation of
// additional instances.
Terminal() = default;
Terminal(const Terminal &) = delete;
Terminal &operator=(const Terminal &) = delete;

void select_below();
void select_above();
void clear_list(int list_size);

// Properties.
struct termios original_termios_;
};
} // namespace qupp::io
126 changes: 119 additions & 7 deletions src/io/unix/io.cc
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
#include "qupp/io/io.h"

#include <iostream> // std::cout, std::endl
#include <iostream> // std::cout, std::endl
#include <termios.h> // tcgetattr, tcsetattr
#include <unistd.h> // STDIN_FILENO

namespace qupp::io {
Terminal::Terminal() noexcept { tcgetattr(STDIN_FILENO, &original_termios_); }

Terminal::~Terminal() { tcsetattr(STDIN_FILENO, TCSANOW, &original_termios_); }

Terminal &Terminal::get_instance() {
static Terminal instance;
return instance;
Expand All @@ -11,18 +17,124 @@ Terminal &Terminal::get_instance() {
void Terminal::print(const std::string &message, bool break_line) {
std::cout << message;
if (break_line) {
std::cout << std::endl;
std::cout << "\n";
}
}

void Terminal::move_cursor_left(int n) { std::cout << "\033[" << n << "D"; }
void Terminal::move_cursor_right(int n) { std::cout << "\033[" << n << "C"; }
void Terminal::move_cursor_up(int n) { std::cout << "\033[" << n << "A"; }
void Terminal::move_cursor_down(int n) { std::cout << "\033[" << n << "B"; }
void Terminal::print_with_prefix(const std::string &prefix,
const std::string &message, bool break_line) {
std::cout << prefix << " " << message;
if (break_line) {
std::cout << "\n";
}
}

std::string Terminal::read() {
std::string Terminal::read_from_console() {
std::string input;
std::cin >> input;

move_cursor_up(1);
clear_line();

return input;
}

void Terminal::select_above() {
std::cout << "[ ]";
move_cursor_up(1);
move_cursor_left(1000);
std::cout << "[*]";
move_cursor_left(1000);
}

void Terminal::select_below() {
std::cout << "[ ]";
move_cursor_down(1);
move_cursor_left(1000);
std::cout << "[*]";
move_cursor_left(100);
}

void Terminal::clear_list(int list_size) {
// Move cursor to the bottom of the list
move_cursor_down(list_size);
int current_position = list_size;

// Clear all options
while (current_position--) {
clear_line();
move_cursor_up(1);
}

// Clear question
clear_line();
move_cursor_up(1);
}

int Terminal::read_in_list(int list_size) {
int selected_item = 0;
while (true) {
char c;
read(STDIN_FILENO, &c, 1);

if (c == '\x1b') {
// If the first value is a 27, then it is an escape sequence.
// The next two characters will indicate the direction.
char sequence[2];
read(STDIN_FILENO, &sequence[0], 1);
read(STDIN_FILENO, &sequence[1], 1);

if (sequence[0] == '[') {
if (sequence[1] == 'A') {
// Up arrow.
if (selected_item > 0) {
select_above();
selected_item--;
}
} else if (sequence[1] == 'B') {
// Down arrow.
if (selected_item < list_size - 1) {
select_below();
selected_item++;
}
}
}
// Enter key.
} else if (c == '\n') {
clear_list(list_size);
return selected_item;
}
}
}

void Terminal::move_cursor_left(int n) {
std::cout << "\033[" << std::to_string(n) << "D" << std::flush;
}

void Terminal::move_cursor_right(int n) {
std::cout << "\033[" << std::to_string(n) << "C" << std::flush;
}

void Terminal::move_cursor_up(int n) {
std::cout << "\033[" << std::to_string(n) << "A" << std::flush;
}

void Terminal::move_cursor_down(int n) {
std::cout << "\033[" << std::to_string(n) << "B" << std::flush;
}

void Terminal::clear_line() {
// 2 stand for EOL
std::cout << "\033[" + std::to_string(2) + "K" << std::flush;
}

void Terminal::enable_raw_mode() {
struct termios raw = original_termios_;
raw.c_lflag &= ~(ECHO | ICANON);
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}

void Terminal::disable_raw_mode() {
tcsetattr(STDIN_FILENO, TCSANOW, &original_termios_);
}
} // namespace qupp::io
9 changes: 6 additions & 3 deletions src/prompt/include/qupp/prompt/prompt.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@

#include <optional>
#include <string>
#include <vector>

namespace qupp::prompt {

using StringResult = std::optional<std::string>;

struct Prompt {
// The explicit keyword is used to avoid implicit conversions.
Prompt(qupp::io::TerminalInterface &terminal =
qupp::io::Terminal::get_instance());

StringResult ask_for_input(const std::string &message);
std::optional<std::string> ask_for_input(const std::string &message);

std::optional<std::string>
select_from_list(const std::string &message,
const std::vector<std::string> &options);

private:
qupp::io::TerminalInterface &terminal_;
Expand Down
36 changes: 33 additions & 3 deletions src/prompt/unix/prompt.cc
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
#include "qupp/prompt/prompt.h"

#include <iostream> // std::cout, std::endl
#include <string> // std::string

namespace qupp::prompt {
Prompt::Prompt(qupp::io::TerminalInterface &terminal) : terminal_(terminal) {}

StringResult Prompt::ask_for_input(const std::string &question) {
std::optional<std::string> Prompt::ask_for_input(const std::string &question) {
if (question.empty()) {
return std::nullopt;
}
terminal_.print(question, false);
terminal_.move_cursor_right(1);
return terminal_.read();
return terminal_.read_from_console();
}

std::optional<std::string>
Prompt::select_from_list(const std::string &question,
const std::vector<std::string> &options) {
if (question.empty() || options.empty()) {
return std::nullopt;
}

terminal_.print(question, true);
terminal_.enable_raw_mode();

for (const auto &option : options) {
bool is_first_option = options.front() == option;
if (is_first_option) {
terminal_.print_with_prefix("[*]", option, true);
} else {
terminal_.print_with_prefix("[ ]", option, true);
}

bool is_last_option = options.back() == option;
if (is_last_option) {
// Move cursor back to the first option.
terminal_.move_cursor_up(options.size());
}
}

int selected_items = terminal_.read_in_list(options.size());

terminal_.disable_raw_mode();
return options[selected_items];
}
} // namespace qupp::prompt
Loading

0 comments on commit c342e8a

Please sign in to comment.