diff options
author | Benjamin Barenblat <bbarenblat@gmail.com> | 2021-12-14 12:34:06 -0500 |
---|---|---|
committer | Benjamin Barenblat <bbarenblat@gmail.com> | 2021-12-14 12:34:06 -0500 |
commit | a4635fb95235ba4bf077bd59957da0626fc5ba72 (patch) | |
tree | d029b32b6668adb1412e63d775928e0cb1ceef39 /src/ui |
EC, a terminal-based RPN calculator
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/stream.cc | 84 | ||||
-rw-r--r-- | src/ui/stream.h | 56 | ||||
-rw-r--r-- | src/ui/terminal.cc | 184 | ||||
-rw-r--r-- | src/ui/terminal.h | 35 | ||||
-rw-r--r-- | src/ui/terminal/line.cc | 206 | ||||
-rw-r--r-- | src/ui/terminal/line.h | 101 |
6 files changed, 666 insertions, 0 deletions
diff --git a/src/ui/stream.cc b/src/ui/stream.cc new file mode 100644 index 0000000..74e34be --- /dev/null +++ b/src/ui/stream.cc @@ -0,0 +1,84 @@ +// 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/stream.h" + +#include <assert.h> + +#include <istream> +#include <memory> +#include <ostream> +#include <string> + +#include "src/error.h" +#include "src/language.h" +#include "src/parser_driver.h" +#include "third_party/abseil/absl/strings/str_join.h" + +namespace ec { + +namespace { + +std::string ReadAll(std::istream& in) noexcept { + std::string result; + int bytes_read = 0; + while (in.good()) { + result.append(4096, '\0'); + in.read(result.data() + bytes_read, 4096); + bytes_read += in.gcount(); + } + assert(bytes_read <= result.size()); + result.resize(bytes_read); + return result; +} + +void ShowState(const State& state, std::ostream& out) noexcept { + if (state.stack.empty()) { + return; + } + out << absl::StrJoin(state.stack, " ", FormatStackElement) << '\n'; +} + +} // namespace + +int LineUi::Main() noexcept { + State state; + while (in_.good()) { + std::string line; + std::getline(in_, line); + try { + EvaluateAll(ParseFromString(line), state); + } catch (const Error& e) { + std::cerr << "ec: " << e.what() << '\n'; + return 1; + } + } + ShowState(state, out_); + return 0; +} + +int StreamUi::Main() noexcept { + State state; + std::string program = ReadAll(in_); + try { + EvaluateAll(ParseFromString(program), state); + } catch (const Error& e) { + std::cerr << "ec: " << e.what() << '\n'; + return 1; + } + ShowState(state, out_); + return 0; +} + +} // namespace ec diff --git a/src/ui/stream.h b/src/ui/stream.h new file mode 100644 index 0000000..106560b --- /dev/null +++ b/src/ui/stream.h @@ -0,0 +1,56 @@ +// 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. + +// Stream-oriented interfaces to EC. + +#ifndef EC_SRC_UI_STREAM_H_ +#define EC_SRC_UI_STREAM_H_ + +#include <iostream> +#include <istream> +#include <ostream> + +#include "src/ui.h" + +namespace ec { + +// A line-mode interpreter working on streams. +class LineUi : public Ui { + public: + explicit LineUi(std::istream& in = std::cin, std::ostream& out = std::cout) + : in_(in), out_(out) {} + + int Main() noexcept override; + + private: + std::istream& in_; + std::ostream& out_; +}; + +// A batch-mode interpreter working on streams. +class StreamUi : public Ui { + public: + explicit StreamUi(std::istream& in = std::cin, std::ostream& out = std::cout) + : in_(in), out_(out) {} + + int Main() noexcept override; + + private: + std::istream& in_; + std::ostream& out_; +}; + +} // namespace ec + +#endif // EC_SRC_UI_STREAM_H_ diff --git a/src/ui/terminal.cc b/src/ui/terminal.cc new file mode 100644 index 0000000..ccb9c4d --- /dev/null +++ b/src/ui/terminal.cc @@ -0,0 +1,184 @@ +// 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.h" + +#include <memory> +#include <string> +#include <vector> + +#include "src/builtin.h" +#include "src/error.h" +#include "src/language.h" +#include "src/parser_driver.h" +#include "src/ui/terminal/line.h" +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/strings/str_join.h" +#include "third_party/abseil/absl/strings/string_view.h" + +namespace ec { + +int TerminalUi::Main() noexcept { + BlockSigwinch(); + TerminalLine tty; + + State machine_state; + std::string input_buffer; + while (true) { + tty.SetLineImmediately(absl::StrCat( + absl::StrJoin(machine_state.stack, " ", FormatStackElement), " > ", + input_buffer)); + + char c = tty.GetChar(); + if (c == tty.interrupt_char() || c == tty.quit_char()) { + // Somebody hit Ctrl-C or Ctrl-\. + return 0; + } + switch (c) { + case kControlD: + if (input_buffer.empty()) { + return 0; + } + tty.Beep(); + break; + + case kControlU: + input_buffer.clear(); + break; + + case '\n': + case '\r': + case ' ': + try { + std::vector<std::shared_ptr<const Term>> program; + if (input_buffer.empty()) { + program = {std::make_shared<ForeignProgramTerm>(BuiltinDup)}; + } else { + program = ParseFromString(input_buffer); + } + + State s = machine_state; + EvaluateAll(program, s); + machine_state = s; + input_buffer.clear(); + } catch (const Error& e) { + tty.Beep(); + } + break; + + case '\x7f': + try { + if (input_buffer.empty()) { + State s = machine_state; + ForeignProgramTerm(BuiltinDrop).Evaluate(s); + machine_state = s; + } else { + input_buffer.pop_back(); + } + } catch (const Error& e) { + tty.Beep(); + } + break; + + case '+': + case '-': + case '*': + case '/': + try { + std::vector<std::shared_ptr<const Term>> program = + ParseFromString(absl::StrCat(input_buffer, std::string(1, c))); + State s = machine_state; + EvaluateAll(program, s); + machine_state = s; + input_buffer.clear(); + } catch (const Error& e) { + tty.Beep(); + } + break; + + case '.': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + case '_': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + case 'h': + case 'i': + case 'j': + case 'k': + case 'l': + case 'm': + case 'n': + case 'o': + case 'p': + case 'q': + case 'r': + case 's': + case 't': + case 'u': + case 'v': + case 'w': + case 'x': + case 'y': + case 'z': + input_buffer.push_back(c); + break; + + default: + tty.Beep(); + break; + } + } +} + +} // namespace ec diff --git a/src/ui/terminal.h b/src/ui/terminal.h new file mode 100644 index 0000000..22f5d01 --- /dev/null +++ b/src/ui/terminal.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef EC_SRC_UI_TERMINAL_H_ +#define EC_SRC_UI_TERMINAL_H_ + +#include "src/ui.h" + +namespace ec { + +// An interactive interpreter targeted at terminal sessions. +class TerminalUi : public Ui { + public: + explicit TerminalUi() = default; + + TerminalUi(const TerminalUi&) = delete; + TerminalUi& operator=(const TerminalUi&) = delete; + + int Main() noexcept override; +}; + +} // namespace ec + +#endif // EC_SRC_UI_TERMINAL_H_ 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 diff --git a/src/ui/terminal/line.h b/src/ui/terminal/line.h new file mode 100644 index 0000000..a90e2b7 --- /dev/null +++ b/src/ui/terminal/line.h @@ -0,0 +1,101 @@ +// 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. + +// A terminal driver for single-line UIs. + +#ifndef EC_SRC_UI_TERMINAL_LINE_H_ +#define EC_SRC_UI_TERMINAL_LINE_H_ + +#include <termios.h> + +#include <thread> + +#include "third_party/abseil/absl/base/thread_annotations.h" +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/synchronization/mutex.h" + +namespace ec { + +// The driver itself. +// +// This class is thread-safe, but there are still some sharp edges. See the +// warning in the constructor documentation about additional steps required to +// use this class in a multi-threaded program. +class TerminalLine final { + public: + // Starts driving the standard input and standard output of the process. + // + // Multi-threading warning: You must block SIGWINCH in all your program's + // threads before constructing an instance of this class. This class detects + // failure to block SIGWINCH in the calling thread, but it cannot check all + // the threads. It is your responsibility to block SIGWINCH everywhere! + // (BlockSigwinch is a convenience function to do this in the current thread.) + explicit TerminalLine(); + + TerminalLine(const TerminalLine&) = delete; + TerminalLine& operator=(const TerminalLine&) = delete; + + ~TerminalLine() noexcept; + + void SetLine(std::string) ABSL_LOCKS_EXCLUDED(mu_); + void Refresh() ABSL_LOCKS_EXCLUDED(mu_); + + void SetLineImmediately(std::string text) { + SetLine(text); + Refresh(); + } + + char GetChar(); + char interrupt_char() const noexcept { return current_termios_.c_cc[VINTR]; } + char quit_char() const noexcept { return current_termios_.c_cc[VQUIT]; } + char suspend_char() const noexcept { return current_termios_.c_cc[VSUSP]; } +#ifdef VDSUSP + char delayed_suspend_char() const noexcept { + return current_termios_.c_cc[VDSUSP]; + } +#endif + + void Beep() ABSL_LOCKS_EXCLUDED(mu_); + + // Prints the specified message on a new line, and redisplays the original + // text on the line after that. + void PrintLine(absl::string_view) ABSL_LOCKS_EXCLUDED(mu_); + + private: + void EnterRawMode(); + void ExitRawMode() noexcept; + + void ReportSigwinch() ABSL_LOCKS_EXCLUDED(mu_); + + void WriteRaw(absl::string_view bytes) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); + + termios original_termios_, current_termios_; + + std::thread sigwinch_watcher_; + + absl::Mutex mu_; + int columns_ ABSL_GUARDED_BY(mu_); + std::string line_ ABSL_GUARDED_BY(mu_); +}; + +// Names for control characters. +constexpr char kControlD = '\x04'; +constexpr char kControlU = '\x15'; + +// A convenience function to block SIGWINCH in the calling thread. +void BlockSigwinch(); + +} // namespace ec + +#endif // EC_SRC_UI_TERMINAL_LINE_H_ |