| // Copyright 2019 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/modal_line_input.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <locale> |
| |
| namespace line_input { |
| |
| namespace { |
| |
| // State associated with running a modal options prompt. |
| class ModalOptionState { |
| public: |
| ModalOptionState(ModalLineInput* input, ModalPromptOptions opts, |
| ModalLineInput::ModalCompletionCallback cb) |
| : input_(input), options_(std::move(opts)), on_complete_(std::move(cb)) {} |
| |
| void OnAccept(const std::string& line) { CheckAccept(line, true); } |
| |
| void OnChanged(const std::string& line) { |
| if (options_.require_enter) |
| return; // Nothing to do. |
| |
| if (CheckAccept(line, false)) { |
| // When the user has typed valid input and we don't require enter, synthesize an enter to |
| // invoke the normal accept codepath. We could close the input now, but skipping tne |
| // enter will erase the current line in normal console mode. |
| // |
| // This will cause OnAccept() above to be called which will then signal completion. |
| input_->OnInput('\r'); |
| } |
| } |
| |
| void OnCancel() { |
| if (!options_.cancel_option.empty()) { |
| input_->EndModal(); |
| on_complete_(options_.cancel_option); |
| } |
| } |
| |
| private: |
| // Checks whether the current line is a valid option. On success, returns true and optionally |
| // signals completion (which closes the modal prompt). |
| bool CheckAccept(const std::string& line, bool signal_complete) { |
| // Optionally check case-insensitively. |
| std::string to_check(line); |
| if (!options_.case_sensitive) { |
| for (size_t i = 0; i < to_check.size(); i++) |
| to_check[i] = std::tolower(to_check[i]); |
| } |
| |
| for (const auto& opt : options_.options) { |
| if (to_check == opt) { |
| if (signal_complete) { |
| input_->EndModal(); |
| on_complete_(to_check); |
| } |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| ModalLineInput* input_; // Non-owning (it owns us). |
| ModalPromptOptions options_; |
| ModalLineInput::ModalCompletionCallback on_complete_; |
| }; |
| |
| } // namespace |
| |
| void ModalLineInput::Init(AcceptCallback accept_cb, const std::string& prompt) { |
| FX_DCHECK(!normal_input_) << "Calling Init() twice."; |
| |
| normal_input_ = MakeAndSetupLineInput(std::move(accept_cb), prompt); |
| current_input_ = normal_input_.get(); |
| } |
| |
| void ModalLineInput::SetAutocompleteCallback(AutocompleteCallback cb) { |
| FX_DCHECK(normal_input_) << "Need to call Init() first."; |
| // Autocomplete only works for the non-modal input. |
| normal_input_->SetAutocompleteCallback(std::move(cb)); |
| } |
| |
| void ModalLineInput::SetChangeCallback(ChangeCallback cb) { |
| // Change callbacks only go to the non-modal input. Our model interface handles changes on the |
| // modal one. |
| normal_input_->SetChangeCallback(std::move(cb)); |
| } |
| |
| void ModalLineInput::SetCancelCallback(CancelCallback cb) { |
| // The cancel callback goes only to the regular input. Modal prompts have special handling. |
| normal_input_->SetCancelCallback(std::move(cb)); |
| } |
| |
| void ModalLineInput::SetEofCallback(EofCallback cb) { eof_callback_ = std::move(cb); } |
| |
| void ModalLineInput::SetMaxCols(size_t max) { |
| FX_DCHECK(normal_input_) << "Need to call Init() first."; |
| |
| max_cols_ = max; |
| normal_input_->SetMaxCols(max); |
| if (modal_input_) |
| modal_input_->SetMaxCols(max); |
| } |
| |
| const std::string& ModalLineInput::GetLine() const { return current_input_->GetLine(); } |
| |
| const std::deque<std::string>& ModalLineInput::GetHistory() const { |
| // History always comes from the regular one. The modal input has no history. |
| FX_DCHECK(normal_input_) << "Need to call Init() first."; |
| return normal_input_->GetHistory(); |
| } |
| |
| void ModalLineInput::OnInput(char c) { |
| if (to_delete_) |
| to_delete_.reset(); |
| current_input_->OnInput(c); |
| } |
| |
| void ModalLineInput::AddToHistory(const std::string& line) { |
| // History always goes to the normal input. |
| normal_input_->AddToHistory(line); |
| } |
| |
| void ModalLineInput::Hide() { |
| hidden_ = true; |
| current_input_->Hide(); |
| } |
| |
| void ModalLineInput::Show() { |
| hidden_ = false; |
| current_input_->Show(); |
| } |
| |
| void ModalLineInput::ModalGetOption(const ModalPromptOptions& options, const std::string& prompt, |
| ModalCompletionCallback cb, WillShowModalCallback will_show) { |
| auto state = std::make_shared<ModalOptionState>(this, options, std::move(cb)); |
| |
| // This will show callback registers our changed callback on the new modal input class and |
| // calls the user will_show callback if provided. |this| can be captured here because this |
| // owns the callback. |
| auto do_will_show = [this, state, will_show = std::move(will_show)]() mutable { |
| modal_input_->SetChangeCallback([state](const std::string& line) { state->OnChanged(line); }); |
| modal_input_->SetCancelCallback([state]() { state->OnCancel(); }); |
| if (will_show) |
| will_show(); |
| }; |
| |
| BeginModal( |
| prompt, [state](const std::string& line) { state->OnAccept(line); }, std::move(do_will_show)); |
| } |
| |
| void ModalLineInput::BeginModal(const std::string& prompt, ModalCompletionCallback cb, |
| WillShowModalCallback will_show) { |
| auto& record = modal_callbacks_.emplace_back(); |
| record.prompt = prompt; |
| record.complete = std::move(cb); |
| record.will_show = std::move(will_show); |
| |
| if (!modal_input_) { |
| // Not showing a modal input already, switch to it. |
| if (!hidden_) |
| normal_input_->Hide(); |
| ShowNextModal(); |
| } |
| // Otherwise we're already showing a modal input. This new one will be automatically shown in |
| // time. |
| } |
| |
| void ModalLineInput::EndModal() { |
| FX_DCHECK(modal_input_) << "Not in a modal input."; |
| FX_DCHECK(!modal_callbacks_.empty()); |
| |
| modal_callbacks_.pop_front(); |
| |
| if (!hidden_) |
| modal_input_->Hide(); |
| |
| // Schedule the modal input to be deleted in next OnInput() call to prevent reentrancy. |
| FX_DCHECK(!to_delete_); |
| to_delete_ = std::move(modal_input_); |
| |
| current_input_ = normal_input_.get(); |
| |
| if (modal_callbacks_.empty()) { |
| // Go back to normal mode. |
| if (!hidden_) |
| normal_input_->Show(); |
| } else { |
| ShowNextModal(); |
| } |
| } |
| |
| void ModalLineInput::ShowNextModal() { |
| FX_DCHECK(!modal_callbacks_.empty()); |
| FX_DCHECK(!modal_input_); |
| |
| auto& record = modal_callbacks_.front(); |
| |
| modal_input_ = MakeAndSetupLineInput(std::move(record.complete), record.prompt); |
| current_input_ = modal_input_.get(); |
| |
| if (record.will_show) |
| record.will_show(); |
| |
| if (!hidden_) |
| current_input_->Show(); |
| } |
| |
| std::unique_ptr<LineInput> ModalLineInput::MakeAndSetupLineInput(AcceptCallback accept_cb, |
| const std::string& prompt) { |
| auto input = MakeLineInput(std::move(accept_cb), prompt); |
| |
| if (max_cols_) |
| input->SetMaxCols(max_cols_); |
| |
| input->SetEofCallback([this]() { |
| if (eof_callback_) |
| eof_callback_(); |
| }); |
| |
| return input; |
| } |
| |
| } // namespace line_input |