blob: e0a01fe818d6ee8007e55a41dcd18199c2a8acdd [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/lib/line_input/line_input.h"
#include <stdio.h>
#include <unistd.h>
#ifdef __Fuchsia__
#include <fuchsia/hardware/pty/c/fidl.h>
#include <lib/fdio/io.h>
#include <lib/fdio/unsafe.h>
#else
#include <sys/ioctl.h>
#include <termios.h>
#endif
#include "src/lib/fxl/logging.h"
#include "src/lib/fxl/strings/split_string.h"
namespace line_input {
const char* SpecialCharacters::kTermBeginningOfLine = "\r";
const char* SpecialCharacters::kTermClearToEnd = "\x1b[0K";
const char* SpecialCharacters::kTermCursorToColFormat = "\r\x1b[%dC";
namespace {
size_t GetTerminalMaxCols(int fileno) {
#ifdef __Fuchsia__
if (isatty(STDIN_FILENO)) {
fdio_t* io = fdio_unsafe_fd_to_io(STDIN_FILENO);
fuchsia_hardware_pty_WindowSize wsz;
zx_status_t status;
zx_status_t call_status =
fuchsia_hardware_pty_DeviceGetWindowSize(fdio_unsafe_borrow_channel(io), &status, &wsz);
fdio_unsafe_release(io);
if (call_status != ZX_OK || status != ZX_OK) {
return 0;
}
return wsz.width;
}
#else
struct winsize ws;
if (ioctl(fileno, TIOCGWINSZ, &ws) != -1)
return ws.ws_col;
#endif
return 0; // 0 means disable scrolling.
}
} // namespace
LineInputEditor::LineInputEditor(AcceptCallback accept_cb, const std::string& prompt)
: accept_callback_(std::move(accept_cb)), prompt_(prompt) {
// Start with a blank item at [0] which is where editing will take place.
history_.emplace_front();
}
LineInputEditor::~LineInputEditor() { EnsureNoRawMode(); }
void LineInputEditor::SetAutocompleteCallback(AutocompleteCallback cb) {
autocomplete_callback_ = std::move(cb);
}
void LineInputEditor::SetChangeCallback(ChangeCallback cb) { change_callback_ = std::move(cb); }
void LineInputEditor::SetCancelCallback(CancelCallback cb) { cancel_callback_ = std::move(cb); }
void LineInputEditor::SetEofCallback(EofCallback cb) { eof_callback_ = std::move(cb); }
void LineInputEditor::SetMaxCols(size_t max) { max_cols_ = max; }
const std::string& LineInputEditor::GetLine() const { return history_[history_index_]; }
const std::deque<std::string>& LineInputEditor::GetHistory() const { return history_; }
void LineInputEditor::OnInput(char c) {
FX_DCHECK(visible_); // Don't call while hidden.
// Reverse history mode does its own input handling.
if (reverse_history_mode_) {
HandleReverseHistory(c);
return;
}
if (reading_escaped_input_) {
HandleEscapedInput(c);
return;
}
if (completion_mode_) {
// Special keys for completion mode.
if (c == SpecialCharacters::kKeyTab) {
HandleTab();
return;
}
// We don't handle escape here to cancel because that's ambiguous with
// escape sequences like arrow keys.
AcceptCompletion();
// Fall through to normal key processing.
}
switch (c) {
case SpecialCharacters::kKeyControlA:
MoveHome();
break;
case SpecialCharacters::kKeyControlB:
MoveLeft();
break;
case SpecialCharacters::kKeyControlC:
CancelCommand();
break;
case SpecialCharacters::kKeyControlD:
if (cur_line().empty()) {
HandleEndOfFile();
return;
} else {
HandleDelete();
}
break;
case SpecialCharacters::kKeyControlE:
MoveEnd();
break;
case SpecialCharacters::kKeyControlF:
MoveRight();
break;
case SpecialCharacters::kKeyControlK:
DeleteToEnd();
break;
case SpecialCharacters::kKeyFormFeed:
HandleFormFeed();
break;
case SpecialCharacters::kKeyTab:
HandleTab();
break;
case SpecialCharacters::kKeyNewline: // == Ctrl + J
case SpecialCharacters::kKeyEnter: // == Ctrl + M
HandleEnter();
return;
case SpecialCharacters::kKeyControlN:
MoveDown();
break;
case SpecialCharacters::kKeyControlP:
MoveUp();
break;
case SpecialCharacters::kKeyControlR:
StartReverseHistoryMode();
break;
case SpecialCharacters::kKeyControlT:
TransposeLastTwoCharacters();
break;
case SpecialCharacters::kKeyControlU:
HandleNegAck();
break;
case SpecialCharacters::kKeyControlW:
HandleEndOfTransimission();
break;
case SpecialCharacters::kKeyEsc:
reading_escaped_input_ = true;
break;
case SpecialCharacters::kKeyControlH:
case SpecialCharacters::kKeyBackspace:
HandleBackspace();
break;
default:
Insert(c);
break;
}
}
void LineInputEditor::AddToHistory(const std::string& line) {
if (line.empty())
return;
if (history_.size() > 1 && history_[1] == line)
return;
if (history_.size() == max_history_)
history_.pop_back();
// Editing takes place at history_[0], so this replaces it and pushes
// everything else back with a new blank line to edit.
history_[0] = line;
history_.emplace_front();
}
void LineInputEditor::Hide() {
if (!visible_)
return; // Already hidden.
visible_ = false;
std::string cmd;
cmd += SpecialCharacters::kTermBeginningOfLine;
cmd += SpecialCharacters::kTermClearToEnd;
Write(cmd);
EnsureNoRawMode();
}
void LineInputEditor::Show() {
if (visible_)
return; // Already shown.
visible_ = true;
RepaintLine();
}
void LineInputEditor::HandleEscapedInput(char c) {
// Escape sequences are two bytes, buffer until we have both.
escape_sequence_.push_back(c);
if (escape_sequence_.size() < 2)
return;
if (escape_sequence_.size() < 3 && escape_sequence_[0] == '[' && escape_sequence_[1] >= '0' &&
escape_sequence_[1] <= '9') {
// This is a three-character escape sequence but we've only received two. Wait for more.
return;
}
// Clear the escaped state before running any functions. Some of them can change the input
// which can in turn issue callbacks which can cause other stuff to happen, and we want to be
// in a fresh state if it does.
reading_escaped_input_ = false;
std::string sequence = escape_sequence_;
escape_sequence_.clear();
// See https://en.wikipedia.org/wiki/ANSI_escape_code for escape codes.
if (sequence[0] == '[') {
if (sequence[1] >= '0' && sequence[1] <= '9') {
// 3-character extended sequence.
if (sequence.size() < 3)
return; // Wait for another character.
if (sequence[1] == '3' && sequence[2] == '~') {
HandleDelete();
} else if (sequence[1] == '1' && sequence[2] == '~') {
MoveHome();
} else if (sequence[1] == '4' && sequence[2] == '~') {
MoveEnd();
}
} else {
// Two-character '[' sequence.
switch (sequence[1]) {
case 'A':
MoveUp();
break;
case 'B':
MoveDown();
break;
case 'C':
MoveRight();
break;
case 'D':
MoveLeft();
break;
case 'H':
MoveHome();
break;
case 'F':
MoveEnd();
break;
}
}
} else if (sequence[0] == '0') {
switch (sequence[1]) {
case 'H':
MoveHome();
break;
case 'F':
MoveEnd();
break;
}
}
}
void LineInputEditor::HandleBackspace() {
if (pos_ == 0)
return;
pos_--;
cur_line().erase(pos_, 1);
LineChanged();
}
void LineInputEditor::HandleDelete() {
if (pos_ < cur_line().size()) {
cur_line().erase(pos_, 1);
LineChanged();
}
}
void LineInputEditor::HandleEnter() {
Write("\r\n");
if (history_.size() == max_history_)
history_.pop_back();
std::string new_line = cur_line();
history_[0] = new_line;
EnsureNoRawMode();
accept_callback_(GetLine());
ResetLineState();
if (visible_)
RepaintLine();
}
void LineInputEditor::HandleTab() {
if (!autocomplete_callback_)
return; // Can't do completions.
if (!completion_mode_) {
completions_ = autocomplete_callback_(cur_line());
completion_index_ = 0;
if (completions_.empty())
return; // No completions, don't enter completion mode.
// Transition to tab completion mode.
completion_mode_ = true;
line_before_completion_ = cur_line();
pos_before_completion_ = pos_;
// Put the current line at the end of the completion stack so tabbing
// through wraps around to it.
completions_.push_back(line_before_completion_);
} else {
// Advance to the next completion, with wraparound.
completion_index_++;
if (completion_index_ == completions_.size())
completion_index_ = 0;
}
// Show the new completion.
cur_line() = completions_[completion_index_];
pos_ = cur_line().size();
LineChanged();
}
void LineInputEditor::HandleNegAck() {
cur_line() = cur_line().substr(pos_);
pos_ = 0;
LineChanged();
}
void LineInputEditor::HandleEndOfTransimission() {
const auto& line = cur_line();
if (line.empty())
return;
// We search for the last space that's before the cursor.
size_t latest_space = 0;
for (size_t i = 0; i < line.size(); i++) {
if (i >= pos_)
break;
if (line[i] == ' ')
latest_space = i;
}
// Ctrl-w removes from the latest space until the cursor.
std::string new_line;
if (latest_space > 0)
new_line.append(line.substr(0, latest_space + 1));
new_line.append(line.substr(pos_));
size_t diff = line.size() - new_line.size();
pos_ -= diff;
cur_line() = std::move(new_line);
LineChanged();
}
void LineInputEditor::HandleEndOfFile() {
Write("\r\n");
if (eof_callback_)
eof_callback_();
ResetLineState();
if (visible_)
LineChanged();
}
void LineInputEditor::HandleReverseHistory(char c) {
if (reading_escaped_input_) {
// Escape sequences are two bytes, buffer until we have both.
escape_sequence_.push_back(c);
if (escape_sequence_.size() < 2)
return;
if (escape_sequence_[0] == '[') {
if (escape_sequence_[1] >= '0' && escape_sequence_[1] <= '9') {
// 3-character extended sequence.
if (escape_sequence_.size() < 3)
return; // Wait for another character.
}
}
// Any other escape sequence exists reverse history mode.
EndReverseHistoryMode(false);
}
// Only a handful of operations are valid in reverse history mode.
switch (c) {
// Enters selects the current suggestion.
case SpecialCharacters::kKeyEnter:
case SpecialCharacters::kKeyNewline:
EndReverseHistoryMode(true);
break;
// ctrl-r again searches for the next match.
case SpecialCharacters::kKeyControlR:
SearchNextReverseHistory(false);
break;
// Deleting a character starts the search anew.
case SpecialCharacters::kKeyControlH:
case SpecialCharacters::kKeyBackspace:
if (!reverse_history_input_.empty())
reverse_history_input_.resize(reverse_history_input_.size() - 1);
SearchNextReverseHistory(true);
break;
// Almost all special characters end history mode. This is what sh does.
case SpecialCharacters::kKeyControlA:
case SpecialCharacters::kKeyControlB:
case SpecialCharacters::kKeyControlD:
case SpecialCharacters::kKeyControlE:
case SpecialCharacters::kKeyControlF:
case SpecialCharacters::kKeyFormFeed:
case SpecialCharacters::kKeyTab:
case SpecialCharacters::kKeyControlN:
case SpecialCharacters::kKeyControlP:
case SpecialCharacters::kKeyControlU:
case SpecialCharacters::kKeyControlW:
case SpecialCharacters::kKeyEsc:
EndReverseHistoryMode(false);
break;
// Add the input to the current search string and do the lookup anew.
default:
reverse_history_input_.append(1, c);
SearchNextReverseHistory(true);
break;
}
LineChanged();
};
void LineInputEditor::StartReverseHistoryMode() {
FX_DCHECK(!reverse_history_mode_);
reverse_history_mode_ = true;
reverse_history_index_ = 0;
reverse_history_input_.clear();
LineChanged();
}
void LineInputEditor::EndReverseHistoryMode(bool accept_suggestion) {
FX_DCHECK(reverse_history_mode_);
reverse_history_mode_ = false;
if (accept_suggestion) {
cur_line() = GetReverseHistorySuggestion();
pos_ = cur_line().size();
} else {
pos_ = 0;
}
}
void LineInputEditor::SearchNextReverseHistory(bool restart) {
if (restart) {
reverse_history_index_ = 0;
} else {
// We want to find the *next* suggestion after the current one.
reverse_history_index_++;
}
// No input, no search.
if (reverse_history_input_.empty()) {
pos_ = 0;
return;
}
// Search for a history entry that has the input a a substring.
size_t index = reverse_history_index_ == 0 ? 1 : reverse_history_index_;
for (size_t i = index; i < history_.size(); i++) {
const std::string& line = history_[i];
auto cursor_offset = line.find(reverse_history_input_);
if (cursor_offset == std::string::npos)
continue;
// We found a suggestion.
reverse_history_index_ = i;
pos_ = cursor_offset;
return;
}
// If we didn't find a suggestion, we reset the state and clear the state, to indicate to the user
// that it rolled over or it didn't find anything.
reverse_history_index_ = 0;
pos_ = 0;
}
void LineInputEditor::HandleFormFeed() {
Write("\033c"); // Form feed.
LineChanged();
}
void LineInputEditor::Insert(char c) {
if (pos_ == cur_line().size() &&
(max_cols_ == 0 || cur_line().size() + prompt_.size() < max_cols_ - 1)) {
// Append to end and no scrolling needed. Optimize output to avoid
// redrawing the entire line.
cur_line().push_back(c);
pos_++;
Write(std::string(1, c));
if (change_callback_)
change_callback_(cur_line());
} else {
// Insert in the middle.
cur_line().insert(pos_, 1, c);
pos_++;
LineChanged();
}
}
void LineInputEditor::MoveLeft() {
if (pos_ > 0) {
pos_--;
RepaintLine();
}
}
void LineInputEditor::MoveRight() {
if (pos_ < cur_line().size()) {
pos_++;
RepaintLine();
}
}
void LineInputEditor::MoveUp() {
if (history_index_ < history_.size() - 1) {
history_index_++;
pos_ = cur_line().size();
RepaintLine();
}
}
void LineInputEditor::MoveDown() {
if (history_index_ > 0) {
history_index_--;
pos_ = cur_line().size();
RepaintLine();
}
}
void LineInputEditor::MoveHome() {
pos_ = 0;
RepaintLine();
}
void LineInputEditor::MoveEnd() {
pos_ = cur_line().size();
RepaintLine();
}
void LineInputEditor::TransposeLastTwoCharacters() {
if (pos_ >= 2) {
auto swap = cur_line()[pos_ - 1];
cur_line()[pos_ - 1] = cur_line()[pos_ - 2];
cur_line()[pos_ - 2] = swap;
LineChanged();
}
}
void LineInputEditor::CancelCommand() {
if (cancel_callback_) {
cancel_callback_();
} else {
Write("^C\r\n");
ResetLineState();
LineChanged();
}
}
void LineInputEditor::DeleteToEnd() {
if (pos_ != cur_line().size()) {
cur_line().resize(pos_);
LineChanged();
}
}
void LineInputEditor::CancelCompletion() {
cur_line() = line_before_completion_;
pos_ = pos_before_completion_;
completion_mode_ = false;
completions_ = std::vector<std::string>();
LineChanged();
}
void LineInputEditor::AcceptCompletion() {
completion_mode_ = false;
completions_ = std::vector<std::string>();
// Line shouldn't need repainting since this doesn't update it.
}
void LineInputEditor::LineChanged() {
RepaintLine();
if (change_callback_)
change_callback_(cur_line());
}
void LineInputEditor::RepaintLine() {
std::string prompt, line_data;
if (!reverse_history_mode_) {
prompt = prompt_;
line_data = prompt + cur_line();
} else {
prompt = GetReverseHistoryPrompt();
line_data = prompt + GetReverseHistorySuggestion();
}
EnsureRawMode();
std::string buf;
buf.reserve(64);
buf += SpecialCharacters::kTermBeginningOfLine;
// Only print up to max_cols_ - 1 to leave room for the cursor at the end.
size_t pos_in_cols = prompt.size() + pos_;
if (max_cols_ > 0 && line_data.size() >= max_cols_ - 1) {
// Needs scrolling. This code scrolls both the user entry and the prompt.
// This avoids some edge cases where the prompt is wider than the screen.
if (pos_in_cols < max_cols_) {
// Cursor is on the screen with no scrolling, just trim from the right.
line_data.resize(max_cols_);
} else {
// Cursor requires scrolling, position the cursor on the right.
line_data = line_data.substr(pos_in_cols - max_cols_ + 1, max_cols_);
pos_in_cols = max_cols_ - 1;
}
buf += line_data;
} else {
buf += line_data;
}
buf += SpecialCharacters::kTermClearToEnd;
char forward_buf[32];
snprintf(forward_buf, sizeof(forward_buf), SpecialCharacters::kTermCursorToColFormat,
static_cast<int>(pos_in_cols));
buf += forward_buf;
Write(buf);
}
std::string LineInputEditor::GetReverseHistoryPrompt() const {
std::string buf;
buf.reserve(64);
buf += "(reverse-i-search)`";
buf += reverse_history_input_;
buf += "': ";
return buf;
}
std::string LineInputEditor::GetReverseHistorySuggestion() const {
if (reverse_history_input_.empty())
return {};
if (reverse_history_index_ == 0 || reverse_history_index_ >= history_.size())
return {};
return history_[reverse_history_index_];
}
void LineInputEditor::ResetLineState() {
pos_ = 0;
history_index_ = 0;
completion_mode_ = false;
cur_line() = std::string();
}
// LineInputStdout ---------------------------------------------------------------------------------
LineInputStdout::LineInputStdout(AcceptCallback accept_cb, const std::string& prompt)
: LineInputEditor(std::move(accept_cb), prompt) {
SetMaxCols(GetTerminalMaxCols(STDIN_FILENO));
}
LineInputStdout::~LineInputStdout() {}
void LineInputStdout::Write(const std::string& data) {
write(STDOUT_FILENO, data.data(), data.size());
}
void LineInputStdout::EnsureRawMode() {
#if !defined(__Fuchsia__)
if (raw_mode_enabled_)
return;
if (!raw_termios_) {
if (!isatty(STDOUT_FILENO))
return;
// Don't commit until everything succeeds.
original_termios_ = std::make_unique<termios>();
if (tcgetattr(STDOUT_FILENO, original_termios_.get()) == -1)
return;
// Always expect non-raw node to wrap lines for us. Without this, if
// somebody's terminal was left in raw mode when they started the debugger,
// the non-interactive input will be wrapped incorrectly.
original_termios_->c_oflag |= OPOST;
raw_termios_ = std::make_unique<termios>(*original_termios_);
raw_termios_->c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
raw_termios_->c_oflag &= ~(OPOST);
raw_termios_->c_oflag |= OCRNL;
raw_termios_->c_cflag |= CS8;
raw_termios_->c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw_termios_->c_cc[VMIN] = 1;
raw_termios_->c_cc[VTIME] = 0;
}
fflush(stdout); // Synchronize with the buffered stdio stream.
if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, raw_termios_.get()) < 0)
return;
raw_mode_enabled_ = true;
#endif
}
void LineInputStdout::EnsureNoRawMode() {
#if !defined(__Fuchsia__)
if (raw_mode_enabled_) {
fflush(stdout); // Synchronize with the buffered stdio stream.
tcsetattr(STDOUT_FILENO, TCSAFLUSH, original_termios_.get());
raw_mode_enabled_ = false;
}
#endif
}
} // namespace line_input