diff options
Diffstat (limited to 'src/ui/terminal/line.cc')
-rw-r--r-- | src/ui/terminal/line.cc | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/src/ui/terminal/line.cc b/src/ui/terminal/line.cc new file mode 100644 index 0000000..8f156cb --- /dev/null +++ b/src/ui/terminal/line.cc @@ -0,0 +1,206 @@ +// 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 <errno.h> +#include <signal.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +#include <algorithm> +#include <iostream> +#include <stdexcept> +#include <string> +#include <system_error> +#include <thread> +#include <type_traits> + +#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<std::thread::native_handle_type, pthread_t>); + 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<int>(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 |