// 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 "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; } absl::string_view ClearToEol() noexcept { static const std::string kSequence = absl::StrCat("\x1b[K", std::string(3, '\0') /* VT100 padding */); return kSequence; } } // namespace TerminalLine::TerminalLine() { if (!SigwinchBlocked()) { throw std::logic_error("TerminalLine constructed without SIGWINCH blocked"); } EnterRawMode(); 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_); if (line_.size() < columns_) { // We can fit the whole line and the cursor on the screen at once. WriteRaw(absl::StrCat(kBeginningOfLine, line_, ClearToEol())); } else { auto to_display = std::min(line_.size(), columns_ - 4); WriteRaw( absl::StrCat(kBeginningOfLine, "...", absl::string_view(&*line_.end() - to_display, to_display), ClearToEol())); } } 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_); WriteRaw("\a"); } void TerminalLine::PrintLine(absl::string_view message) { { absl::MutexLock lock(&mu_); WriteRaw(absl::StrCat("\r\n", message, "\r\n")); } Refresh(); } void TerminalLine::EnterRawMode() { CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &original_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 TerminalLine::WriteRaw(absl::string_view bytes) { while (true) { int r = write(STDOUT_FILENO, bytes.data(), bytes.size()); if (r >= 0) { return; } else if (errno == EINTR) { continue; } else { throw std::system_error(errno, std::generic_category(), "write"); } } } void BlockSigwinch() { sigset_t sigwinch = SigsetContaining(SIGWINCH); CheckedCall("pthread_sigmask", pthread_sigmask(SIG_BLOCK, &sigwinch, /*oldset=*/nullptr)); } } // namespace ec