// Copyright 2021 Benjamin Barenblat // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "src/ui/terminal/line.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "goldfishterm/simple.h" #include "third_party/abseil/absl/strings/str_cat.h" #include "third_party/abseil/absl/strings/string_view.h" #include "third_party/abseil/absl/synchronization/mutex.h" namespace ec { namespace { constexpr absl::string_view kBeginningOfLine = "\r"; int CheckedCall(const char* what_arg, int r) { if (r < 0) { throw std::system_error(errno, std::generic_category(), what_arg); } return r; } bool SigwinchBlocked() { sigset_t current_blocked; CheckedCall("pthread_sigmask", pthread_sigmask(/*how=*/0, /*set=*/nullptr, ¤t_blocked)); return CheckedCall("sigismember", sigismember(¤t_blocked, SIGWINCH)); } sigset_t SigsetContaining(int signal) { sigset_t set; sigemptyset(&set); CheckedCall("sigaddset", sigaddset(&set, signal)); return set; } int TerminalColumns() { winsize size; CheckedCall("ioctl", ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)); return size.ws_col; } } // namespace TerminalLine::TerminalLine() { if (!SigwinchBlocked()) { throw std::logic_error("TerminalLine constructed without SIGWINCH blocked"); } EnterRawMode(); tty_ = std::make_unique(current_termios_); sigwinch_watcher_ = std::thread([this] { sigset_t sigwinch = SigsetContaining(SIGWINCH); int received; while (true) { sigwait(&sigwinch, &received); ReportSigwinch(); } }); ReportSigwinch(); // initialize state reset on SIGWINCH } TerminalLine::~TerminalLine() noexcept { static_assert(std::is_same_v); pthread_cancel(sigwinch_watcher_.native_handle()); sigwinch_watcher_.join(); ExitRawMode(); // Move the cursor to the start of the next line. std::cout << '\n'; } void TerminalLine::SetLine(std::string text) { absl::MutexLock lock(&mu_); line_ = std::move(text); } void TerminalLine::Refresh() { absl::MutexLock lock(&mu_); tty_->BeginningOfLine(); if (line_.size() < columns_) { // We can fit the whole line and the cursor on the screen at once. tty_->Write(line_); } else { auto to_display = std::min(line_.size(), columns_ - 4); tty_->Write("..."); tty_->Write(absl::string_view(&*line_.end() - to_display, to_display)); } tty_->ClearToEndOfLine(); tty_->Flush(); } char TerminalLine::GetChar() { while (true) { char c; int r = read(STDIN_FILENO, &c, 1); if (r > 0) { return c; } else if (r == 0) { // EOF return kControlD; } else if (errno == EINTR) { continue; } else { throw std::system_error(errno, std::generic_category(), "read"); } } } void TerminalLine::Beep() { absl::MutexLock lock(&mu_); tty_->Beep(); tty_->Flush(); } void TerminalLine::PrintLine(absl::string_view message) { { absl::MutexLock lock(&mu_); tty_->BeginningOfLine(); tty_->CursorDown(); tty_->Write(message); tty_->BeginningOfLine(); tty_->CursorDown(); } Refresh(); // includes a flush } void TerminalLine::EnterRawMode() { CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &original_termios_)); termios current_termios_ = original_termios_; current_termios_.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); current_termios_.c_oflag &= ~(OPOST); current_termios_.c_cflag |= CS8; current_termios_.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); CheckedCall("tcsetattr", tcsetattr(STDIN_FILENO, TCSAFLUSH, ¤t_termios_)); // tcsetattr returns successfully if _any_ of its changes were successful, so // check again to make sure everything went through. termios actual; CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &actual)); if (actual.c_iflag & (BRKINT | ICRNL | INPCK | ISTRIP | IXON) || actual.c_oflag & OPOST || (actual.c_cflag & CS8) != CS8 || actual.c_lflag & (ECHO | ICANON | IEXTEN | ISIG)) { throw std::runtime_error("tcsetattr: could not apply all settings"); } } void TerminalLine::ExitRawMode() noexcept { tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios_); } void TerminalLine::ReportSigwinch() { { absl::MutexLock lock(&mu_); columns_ = TerminalColumns(); } Refresh(); } void BlockSigwinch() { sigset_t sigwinch = SigsetContaining(SIGWINCH); CheckedCall("pthread_sigmask", pthread_sigmask(SIG_BLOCK, &sigwinch, /*oldset=*/nullptr)); } } // namespace ec