aboutsummaryrefslogtreecommitdiff
path: root/src/ui/terminal/line.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/terminal/line.cc')
-rw-r--r--src/ui/terminal/line.cc206
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, &current_blocked));
+ return CheckedCall("sigismember", sigismember(&current_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, &current_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