diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 9 | ||||
-rw-r--r-- | buildconf/asan.ninja | 2 | ||||
-rw-r--r-- | buildconf/common.ninja | 97 | ||||
-rw-r--r-- | buildconf/dbg.ninja | 2 | ||||
-rw-r--r-- | buildconf/fastbuild.ninja | 2 | ||||
-rw-r--r-- | goldfishterm/internal/emit.cc | 47 | ||||
-rw-r--r-- | goldfishterm/internal/emit.h | 106 | ||||
-rw-r--r-- | goldfishterm/internal/string_capability.h | 106 | ||||
-rw-r--r-- | goldfishterm/internal/string_capability.re | 483 | ||||
-rw-r--r-- | goldfishterm/internal/string_capability_test.cc | 372 | ||||
-rw-r--r-- | goldfishterm/simple.cc | 74 | ||||
-rw-r--r-- | goldfishterm/simple.h | 71 | ||||
-rw-r--r-- | goldfishterm/terminfo.cc | 325 | ||||
-rw-r--r-- | goldfishterm/terminfo.h | 544 | ||||
-rw-r--r-- | goldfishterm/terminfo_system_test.cc | 93 | ||||
-rw-r--r-- | goldfishterm/terminfo_test.cc | 228 | ||||
-rwxr-xr-x | scripts/run_all_tests | 16 | ||||
-rw-r--r-- | src/ui/terminal/line.cc | 47 | ||||
-rw-r--r-- | src/ui/terminal/line.h | 3 |
20 files changed, 2587 insertions, 42 deletions
@@ -14,6 +14,8 @@ .ninja_* +goldfishterm/internal/string_capability.cc + *.interp *.tokens src/CalculatorBaseVisitor.* @@ -38,7 +38,8 @@ heavy-duty numerical analysis, EC is not yet the tool for you. To build EC, you’ll need [our customized version of Abseil](https://git.benjamin.barenblat.name/ec-abseil/), which is checked in as a Git submodule. You’ll also need GCC, [Antlr 4](https://www.antlr.org/), -and [Ninja](https://ninja-build.org/), none of which is checked in; on a Debian -system, you can run `apt install build-essential ninja-build antlr4 -libantlr4-runtime-dev` to get the packages you need. Fire up `ninja`, wait a -bit, and you’ll soon have an `ec` binary in the repository root. Enjoy! +[re2c](https://re2c.org/), and [Ninja](https://ninja-build.org/), none of which +is checked in; on a Debian system, you can run `apt install build-essential +ninja-build antlr4 libantlr4-runtime-dev re2c` to get the packages you need. +Fire up `ninja`, wait a bit, and you’ll soon have an `ec` binary in the +repository root. Enjoy! diff --git a/buildconf/asan.ninja b/buildconf/asan.ninja index 8f79f9e..61f1fbc 100644 --- a/buildconf/asan.ninja +++ b/buildconf/asan.ninja @@ -17,4 +17,6 @@ third_party_cxxflags = -fsanitize=address ldflags = -fsanitize=address +re2cflags = -Werror + subninja buildconf/common.ninja diff --git a/buildconf/common.ninja b/buildconf/common.ninja index 256df10..a2dab9d 100644 --- a/buildconf/common.ninja +++ b/buildconf/common.ninja @@ -27,6 +27,8 @@ cxxflags = $common_cxxflags -Wall -Wextra -Wno-logical-op-parentheses $ -Wno-sign-compare $cxxflags third_party_cxxflags = $common_cxxflags $third_party_cxxflags +re2cflags = --empty-class error --no-generation-date -W $re2cflags + rule antlr command = antlr4 $antlrflags $in description = Generating parser from $in @@ -45,6 +47,61 @@ rule link command = g++ $ldflags -o $out $in $libs -pthread description = Linking $out +rule re2c + command = re2c $re2cflags $in -o $out + description = Generating DFAs in $out + +build goldfishterm/internal/emit.o: cxx goldfishterm/internal/emit.cc +build goldfishterm/internal/string_capability.cc: re2c $ + goldfishterm/internal/string_capability.re + re2cflags = $re2cflags +build goldfishterm/internal/string_capability.o: cxx $ + goldfishterm/internal/string_capability.cc +build goldfishterm/simple.o: cxx goldfishterm/simple.cc +build goldfishterm/terminfo.o: cxx goldfishterm/terminfo.cc + +build goldfishterm/internal/string_capability_test.o: cxx $ + goldfishterm/internal/string_capability_test.cc +build goldfishterm/internal/string_capability_test: link $ + goldfishterm/internal/emit.o goldfishterm/internal/string_capability.o $ + goldfishterm/internal/string_capability_test.o goldfishterm/terminfo.o $ + $absl/strings/cord.a $absl/strings/cordz_info.a $ + $absl/strings/cord_internal.a $absl/strings/cordz_functions.a $ + $absl/strings/cordz_handle.a $absl/hash/hash.a $absl/hash/city.a $ + $absl/types/bad_variant_access.a $absl/hash/low_level_hash.a $ + $absl/container/raw_hash_set.a $absl/container/hashtablez_sampler.a $ + $absl/profiling/exponential_biased.a $ + $absl/synchronization/synchronization.a $ + $absl/synchronization/graphcycles_internal.a $absl/debugging/stacktrace.a $ + $absl/debugging/symbolize.a $absl/debugging/debugging_internal.a $ + $absl/debugging/demangle_internal.a $absl/base/malloc_internal.a $ + $absl/strings/str_format_internal.a $absl/time/time.a $ + $absl/time/civil_time.a $absl/strings/strings.a $absl/strings/internal.a $ + $absl/base/base.a $absl/base/spinlock_wait.a $absl/numeric/int128.a $ + $absl/time/time_zone.a $absl/types/bad_optional_access.a $ + $absl/base/throw_delegate.a $ + $absl/base/raw_logging_internal.a $absl/base/log_severity.a + libs = -lgmock_main -lgmock -lgtest -lm -lrt + +build goldfishterm/terminfo_system_test.o: cxx $ + goldfishterm/terminfo_system_test.cc +build goldfishterm/terminfo_system_test: link goldfishterm/terminfo.o $ + goldfishterm/terminfo_system_test.o $absl/types/bad_optional_access.a $ + $absl/strings/str_format_internal.a $absl/strings/strings.a $ + $absl/strings/internal.a $absl/base/base.a $absl/base/spinlock_wait.a $ + $absl/numeric/int128.a $absl/base/throw_delegate.a $ + $absl/base/raw_logging_internal.a $absl/base/log_severity.a + libs = -lrt + +build goldfishterm/terminfo_test.o: cxx goldfishterm/terminfo_test.cc +build goldfishterm/terminfo_test: link goldfishterm/terminfo.o $ + goldfishterm/terminfo_test.o $absl/strings/str_format_internal.a $ + $absl/strings/strings.a $absl/strings/internal.a $absl/base/base.a $ + $absl/base/spinlock_wait.a $absl/numeric/int128.a $ + $absl/base/throw_delegate.a $absl/types/bad_optional_access.a $ + $absl/base/raw_logging_internal.a $absl/base/log_severity.a + libs = -lgmock_main -lgmock -lgtest -lrt + build | src/CalculatorBaseVisitor.cpp src/CalculatorBaseVisitor.h $ src/Calculator.interp src/CalculatorLexer.cpp src/CalculatorLexer.h $ src/CalculatorLexer.interp src/CalculatorLexer.tokens $ @@ -128,21 +185,24 @@ build src/parser_driver_test: link src/builtin.o src/CalculatorBaseVisitor.o $ build ec: link src/CalculatorBaseVisitor.o src/CalculatorLexer.o $ src/CalculatorParser.o src/CalculatorVisitor.o src/builtin.o $ src/language.o src/main.o src/parser_driver.o src/util.o src/ui/stream.o $ - src/ui/terminal.o src/ui/terminal/line.o $absl/strings/cord.a $ - $absl/strings/cordz_info.a $absl/strings/cord_internal.a $ - $absl/strings/cordz_functions.a $absl/strings/cordz_handle.a $ - $absl/hash/hash.a $absl/hash/city.a $absl/types/bad_variant_access.a $ + src/ui/terminal.o src/ui/terminal/line.o goldfishterm/internal/emit.o $ + goldfishterm/internal/string_capability.o goldfishterm/simple.o $ + goldfishterm/terminfo.o $absl/strings/cord.a $absl/strings/cordz_info.a $ + $absl/strings/cord_internal.a $absl/strings/cordz_functions.a $ + $absl/strings/cordz_handle.a $absl/hash/hash.a $absl/hash/city.a $ $absl/hash/low_level_hash.a $absl/container/raw_hash_set.a $ - $absl/types/bad_optional_access.a $absl/container/hashtablez_sampler.a $ - $absl/profiling/exponential_biased.a $ + $absl/container/hashtablez_sampler.a $absl/profiling/exponential_biased.a $ + $absl/strings/str_format_internal.a $ $absl/synchronization/synchronization.a $ $absl/synchronization/graphcycles_internal.a $absl/debugging/stacktrace.a $ $absl/debugging/symbolize.a $absl/debugging/debugging_internal.a $ $absl/debugging/demangle_internal.a $absl/base/malloc_internal.a $ $absl/time/time.a $absl/time/civil_time.a $absl/strings/strings.a $ $absl/strings/internal.a $absl/base/base.a $absl/base/spinlock_wait.a $ - $absl/numeric/int128.a $absl/time/time_zone.a $absl/base/throw_delegate.a $ - $absl/base/raw_logging_internal.a $absl/base/log_severity.a + $absl/numeric/int128.a $absl/time/time_zone.a $ + $absl/types/bad_optional_access.a $absl/base/throw_delegate.a $ + $absl/types/bad_variant_access.a $absl/base/raw_logging_internal.a $ + $absl/base/log_severity.a libs = -lantlr4-runtime -lm -lrt # //absl/base @@ -372,6 +432,27 @@ build $absl/strings/internal/ostringstream.o: cxx $ build $absl/strings/internal/utf8.o: cxx $absl/strings/internal/utf8.cc cxxflags = $third_party_cxxflags +# //absl/strings:str_format_internal +build $absl/strings/str_format_internal.a: ar $ + $absl/strings/internal/str_format/arg.o $ + $absl/strings/internal/str_format/bind.o $ + $absl/strings/internal/str_format/extension.o $ + $absl/strings/internal/str_format/float_conversion.o $ + $absl/strings/internal/str_format/output.o $ + $absl/strings/internal/str_format/parser.o +build $absl/strings/internal/str_format/arg.o: cxx $absl/strings/internal/str_format/arg.cc + cxxflags = $third_party_cxxflags +build $absl/strings/internal/str_format/bind.o: cxx $absl/strings/internal/str_format/bind.cc + cxxflags = $third_party_cxxflags +build $absl/strings/internal/str_format/extension.o: cxx $absl/strings/internal/str_format/extension.cc + cxxflags = $third_party_cxxflags +build $absl/strings/internal/str_format/float_conversion.o: cxx $absl/strings/internal/str_format/float_conversion.cc + cxxflags = $third_party_cxxflags +build $absl/strings/internal/str_format/output.o: cxx $absl/strings/internal/str_format/output.cc + cxxflags = $third_party_cxxflags +build $absl/strings/internal/str_format/parser.o: cxx $absl/strings/internal/str_format/parser.cc + cxxflags = $third_party_cxxflags + # //absl/synchronization build $absl/synchronization/synchronization.a: ar $ $absl/synchronization/barrier.o $absl/synchronization/blocking_counter.o $ diff --git a/buildconf/dbg.ninja b/buildconf/dbg.ninja index cce13eb..4486e80 100644 --- a/buildconf/dbg.ninja +++ b/buildconf/dbg.ninja @@ -15,4 +15,6 @@ cxxflags = -Og -g3 -Werror third_party_cxxflags = -Og -g3 +re2cflags = -Werror + subninja buildconf/common.ninja diff --git a/buildconf/fastbuild.ninja b/buildconf/fastbuild.ninja index c321e30..8cd042a 100644 --- a/buildconf/fastbuild.ninja +++ b/buildconf/fastbuild.ninja @@ -14,4 +14,6 @@ cxxflags = -Werror +re2cflags = -Werror + subninja buildconf/common.ninja diff --git a/goldfishterm/internal/emit.cc b/goldfishterm/internal/emit.cc new file mode 100644 index 0000000..2cc7609 --- /dev/null +++ b/goldfishterm/internal/emit.cc @@ -0,0 +1,47 @@ +// 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 "goldfishterm/internal/emit.h" + +#include <errno.h> +#include <stdint.h> +#include <unistd.h> + +#include <memory> +#include <system_error> +#include <vector> + +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/time/clock.h" +#include "third_party/abseil/absl/types/span.h" + +namespace goldfishterm_internal { + +void EmitBytes::Emit(std::ostream& out) const { + out.write(bytes_.data(), bytes_.size()); +} + +void EmitDelay::Emit(std::ostream& out) const noexcept { + out.flush(); + absl::SleepFor(delay_); +} + +void Emit(absl::Span<const std::shared_ptr<const EmitTerm>> terms, + std::ostream& out) { + for (const auto& term : terms) { + term->Emit(out); + } +} + +} // namespace goldfishterm_internal diff --git a/goldfishterm/internal/emit.h b/goldfishterm/internal/emit.h new file mode 100644 index 0000000..371f6b0 --- /dev/null +++ b/goldfishterm/internal/emit.h @@ -0,0 +1,106 @@ +// 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. + +// An extremely simple EDSL for describing output to the terminal. It supports +// exactly two instructions: "send some bytes to the terminal" and "sleep". The +// string capability interpreter produces terms in this EDSL, which makes it +// easier to test. + +#ifndef EC_GOLDFISHTERM_INTERNAL_EMIT_H_ +#define EC_GOLDFISHTERM_INTERNAL_EMIT_H_ + +#include <stdint.h> + +#include <memory> +#include <ostream> +#include <string> +#include <utility> + +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/time/time.h" +#include "third_party/abseil/absl/types/span.h" + +namespace goldfishterm_internal { + +class EmitTerm { + public: + virtual void Emit(std::ostream&) const = 0; + + virtual ~EmitTerm() noexcept = default; +}; + +class EmitBytes final : public EmitTerm { + public: + explicit EmitBytes() noexcept = default; + + explicit EmitBytes(absl::string_view bytes) noexcept : bytes_(bytes) {} + + EmitBytes(const EmitBytes&) = default; + EmitBytes& operator=(const EmitBytes&) = default; + EmitBytes(EmitBytes&&) noexcept = default; + EmitBytes& operator=(EmitBytes&&) noexcept = default; + + bool empty() const noexcept { return bytes_.empty(); } + + void Append(absl::string_view more) noexcept { + absl::StrAppend(&bytes_, more); + } + + void Emit(std::ostream&) const override; + + bool operator==(const EmitBytes& other) const noexcept { + return bytes_ == other.bytes_; + } + + friend std::ostream& operator<<(std::ostream& out, + const EmitBytes& emit) noexcept { + return out << "EmitBytes(" << emit.bytes_ << ')'; + } + + private: + std::string bytes_; +}; + +class EmitDelay final : public EmitTerm { + public: + explicit EmitDelay() noexcept = default; + + explicit EmitDelay(absl::Duration delay) : delay_(std::move(delay)) {} + + EmitDelay(const EmitDelay&) = default; + EmitDelay& operator=(const EmitDelay&) = default; + EmitDelay(EmitDelay&&) noexcept = default; + EmitDelay& operator=(EmitDelay&&) noexcept = default; + + bool zero() const noexcept { return delay_ == absl::ZeroDuration(); } + + void Increase(absl::Duration more) noexcept { delay_ += more; } + + void Emit(std::ostream&) const noexcept override; + + bool operator==(const EmitDelay& other) const noexcept { + return delay_ == other.delay_; + } + + private: + absl::Duration delay_; +}; + +// Runs an entire program in the emitter EDSL. +void Emit(absl::Span<const std::shared_ptr<const EmitTerm>>, std::ostream&); + +} // namespace goldfishterm_internal + +#endif // EC_GOLDFISHTERM_INTERNAL_EMIT_H_ diff --git a/goldfishterm/internal/string_capability.h b/goldfishterm/internal/string_capability.h new file mode 100644 index 0000000..ddae964 --- /dev/null +++ b/goldfishterm/internal/string_capability.h @@ -0,0 +1,106 @@ +// 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. + +// Parses and interprets terminfo string capabilities. These are written in a +// simple stack-based programming language whose semantics are documented in +// terminfo(5). + +#ifndef EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_ +#define EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_ + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "goldfishterm/internal/emit.h" +#include "goldfishterm/terminfo.h" +#include "third_party/abseil/absl/types/optional.h" +#include "third_party/abseil/absl/types/variant.h" + +namespace goldfishterm_internal { + +using StringCapabilityParameter = absl::variant<int, std::string>; + +struct InterpretStringCapabilityInput { + std::string capability; + std::vector<StringCapabilityParameter> parameters; + + // The number of lines affected by this operation. + int lines_affected; + + // The minimum baud rate at which padding should be expanded. + int padding_baud_rate; + + // The padding character, or absl::nullopt if the terminal doesn't support + // padding. + absl::optional<char> pad_char; + + // True iff the terminal supports XON/XOFF handshaking. + bool has_xon_xoff; + + // The baud rate of the terminal. + int baud; + + // The number of extra bits sent per character. For a terminal emulator, this + // is generally 0; for an RS-232 line, it's generally 2 or 3 (one start bit, + // one stop bit, and possibly one parity bit). + int extra_bits_per_character; + + // Constructs an InterpretStringCapabilityInput with fail-safe defaults. + explicit InterpretStringCapabilityInput() noexcept + : lines_affected(1), + padding_baud_rate(0), + pad_char('\0'), + has_xon_xoff(false), + baud(9600), + extra_bits_per_character(0) {} + + // Constructs an InterpretStringCapabilityInput from a terminfo entry. + explicit InterpretStringCapabilityInput( + const goldfishterm::TerminfoEntry& terminfo, std::string capability_, + std::vector<StringCapabilityParameter> parameters_, int baud_, + int lines_affected_) noexcept + : capability(std::move(capability_)), + parameters(std::move(parameters_)), + lines_affected(lines_affected_), + padding_baud_rate( + terminfo.get(goldfishterm::NumericCapability::kPaddingBaudRate) + .value_or(0)), + pad_char(terminfo.get(goldfishterm::StringCapability::kCharPadding) + .value_or(std::string(1, '\0'))[0]), + has_xon_xoff(terminfo.get(goldfishterm::BooleanCapability::kXonXoff)), + baud(baud_), + extra_bits_per_character(0) {} +}; + +struct InterpretStringCapabilityResult { + // Terms in the emitter EDSL (see emit.h). + std::vector<std::shared_ptr<const EmitTerm>> terms; + + // The expected cost of emitting these terms. This is a unitless, arbitrarily + // scaled real number; it will be at least 0, but you should not rely on any + // absolute semantics. + float cost; +}; + +// Interprets a terminfo string capability, expanding parameters and inserting +// padding and delays as appropriate. Throws std::runtime_error if parsing or +// interpretation fails. +InterpretStringCapabilityResult InterpretStringCapability( + const InterpretStringCapabilityInput&); + +} // namespace goldfishterm_internal + +#endif // EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_ diff --git a/goldfishterm/internal/string_capability.re b/goldfishterm/internal/string_capability.re new file mode 100644 index 0000000..6920066 --- /dev/null +++ b/goldfishterm/internal/string_capability.re @@ -0,0 +1,483 @@ +// 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 <math.h> + +#include <functional> +#include <memory> +#include <stdexcept> +#include <string> +#include <type_traits> +#include <utility> +#include <vector> + +#include "goldfishterm/internal/string_capability.h" +#include "third_party/abseil/absl/container/flat_hash_map.h" +#include "third_party/abseil/absl/strings/match.h" +#include "third_party/abseil/absl/strings/numbers.h" +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/strings/str_format.h" +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/time/time.h" +#include "third_party/abseil/absl/types/variant.h" + +namespace goldfishterm_internal { + +namespace { + +absl::string_view Slice(const char* start, const char* end) noexcept { + return absl::string_view(start, end - start); +} + +// The string capability interpreter, implemented as an re2c-generated DFA. +class Interpreter final { + public: + explicit Interpreter(InterpretStringCapabilityInput input) noexcept + : input_(std::move(input)), + chars_per_ms_(input_.baud / 1000.0 / + (8 * sizeof(char) + input_.extra_bits_per_character)) { + result_.cost = 0; + } + + void Run() { + enum ConditionalEvaluationState { + kNormal, + kEvaluatingThen, + kSkippingThen, + kSkippingElse, + } if_state = kNormal; + + const char* YYCURSOR = input_.capability.c_str(); + const char* YYMARKER; + const char* a; + const char* b; + /*!stags:re2c format = "const char* @@;"; */ + + loop: + /*!re2c + re2c:define:YYCTYPE = char; + re2c:flags:tags = 1; + re2c:yyfill:enable = 0; + + integer = [0-9]+; + decimal = integer ("." integer)?; + + [\x00] { goto done; } + + "$<" @a ( "0"* [1-9] "0"* ("." "0"*)? | "0"* "." "0"* [1-9] "0"* ) @b "*"? + "/"? ">" { + if (if_state == kSkippingThen || if_state == kSkippingElse || + input_.baud < input_.padding_baud_rate) { + goto loop; + } + + float delay_ms; + if (!absl::SimpleAtof(Slice(a, b), &delay_ms)) { + throw std::logic_error( + "goldfishterm: parser produced an invalid float"); + } + bool per_line_pad = *b == '*'; + bool padding_mandatory = *(YYCURSOR - 2) == '/'; + if (per_line_pad) { + delay_ms *= input_.lines_affected; + } + if (input_.has_xon_xoff && !padding_mandatory) { + // Record that we expect a delay, but don't actually insert a delay. + ExpectDelay(absl::Milliseconds(delay_ms)); + } else if (input_.pad_char.has_value()) { + Bytes(std::string(ceilf(delay_ms * chars_per_ms_), *input_.pad_char)); + } else { + Delay(absl::Milliseconds(delay_ms)); + } + goto loop; + } + + "%%" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + Bytes("%"); + goto loop; + } + + "%" @a ((":"? [-+# ]+)? decimal? [doxX] | ":-"? integer? "c") { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + EmitPopped<int>(Slice(a, YYCURSOR)); + goto loop; + } + + "%" @a ":-"? decimal? "s" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + EmitPopped<std::string>(Slice(a, YYCURSOR)); + goto loop; + } + + "%p" [1-9] { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + int index = *(YYCURSOR - 1) - '0'; + try { + stack_.push_back(input_.parameters.at(index - 1)); + } catch (const std::out_of_range& e) { + throw std::runtime_error( + absl::StrCat(absl::StrCat("invalid parameter ", index))); + } + goto loop; + } + + "%P" [A-Za-z] { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + RequireNonempty(); + environment_[*(YYCURSOR - 1)] = std::move(stack_.back()); + stack_.pop_back(); + goto loop; + } + + "%g" [A-Za-z] { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + char var = *(YYCURSOR - 1); + auto it = environment_.find(var); + if (it == environment_.end()) { + throw std::runtime_error(absl::StrCat("undefined variable ", + Slice(YYCURSOR - 1, YYCURSOR))); + } + stack_.push_back(it->second); + goto loop; + } + + "%'" [^\x00] "'" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + stack_.push_back(*(YYCURSOR - 2)); + goto loop; + } + + "%{" @a integer "}" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + int value; + if (!absl::SimpleAtoi(Slice(a, YYCURSOR - 1), &value)) { + throw std::logic_error( + "goldfishterm: parser produced an invalid int"); + } + stack_.push_back(value); + goto loop; + } + + "%l" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + Unop<std::string, int>( + [](const std::string& s) { return static_cast<int>(s.size()); }); + goto loop; + } + + "%+" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::plus<int>()); + goto loop; + } + + "%-" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::minus<int>()); + goto loop; + } + + "%*" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::multiplies<int>()); + goto loop; + } + + "%/" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::divides<int>()); + goto loop; + } + + "%m" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::modulus<int>()); + goto loop; + } + + "%&" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::bit_and<int>()); + goto loop; + } + + "%|" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::bit_or<int>()); + goto loop; + } + + "%^" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::bit_xor<int>()); + goto loop; + } + + "%=" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::equal_to<int>()); + goto loop; + } + + "%>" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::greater<int>()); + goto loop; + } + + "%<" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::less<int>()); + goto loop; + } + + "%A" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::logical_and<int>()); + goto loop; + } + + "%O" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + BinopN(std::logical_or<int>()); + goto loop; + } + + "%!" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + UnopN(std::logical_not<int>()); + goto loop; + } + + "%~" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + UnopN(std::bit_not<int>()); + goto loop; + } + + "%i" { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + if (input_.parameters.size() < 2) { + throw std::runtime_error("%i requires at least two parameters"); + } + auto* one = absl::get_if<int>(&input_.parameters[0]); + auto* two = absl::get_if<int>(&input_.parameters[1]); + RequireType<int>(one); + RequireType<int>(two); + ++*one; + ++*two; + goto loop; + } + + "%?" { + if (if_state != kNormal) { + throw std::logic_error("goldfishterm: unexpected %?"); + } + goto loop; + } + "%t" { + if (if_state == kNormal) { + RequireNonempty(); + int* condition_int = absl::get_if<int>(&stack_.back()); + // Strings are true, as are nonzero integers. + bool condition = condition_int == nullptr || *condition_int; + stack_.pop_back(); + if_state = condition ? kEvaluatingThen : kSkippingThen; + } else if (if_state != kSkippingElse) { + throw std::logic_error("goldfishterm: unexpected %t"); + } + goto loop; + } + "%e" { + switch (if_state) { + case kNormal: + throw std::logic_error("goldfishterm: unexpected %e"); + break; + case kEvaluatingThen: + if_state = kSkippingElse; + break; + case kSkippingThen: + if_state = kNormal; + break; + case kSkippingElse: + break; + } + goto loop; + } + "%;" { + if_state = kNormal; + goto loop; + } + + * { + if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; + + Bytes(Slice(YYCURSOR - 1, YYCURSOR)); + goto loop; + } + */ + + done: + Flush(); + } + + const InterpretStringCapabilityResult& result() noexcept { return result_; } + + private: + void RequireNonempty() { + if (stack_.empty()) { + throw std::runtime_error("stack underflow"); + } + } + + // A companion to absl::get_if: Requires that the passed pointer is nonnull + // and throws an appropriate error if it's not. + template <typename T> + void RequireType(const void* v) { + if (v == nullptr) { + if constexpr (std::is_same_v<T, const int>) { + throw std::runtime_error("type error: expected integer"); + } else if constexpr (std::is_same_v<T, const std::string>) { + throw std::runtime_error("type error: expected string"); + } else { + throw std::runtime_error("type error"); + } + } + } + + // Executes push(f(pop())). + template <typename T, typename R> + void Unop(std::function<R(T)> f) { + RequireNonempty(); + T* v = absl::get_if<T>(&stack_.back()); + RequireType<int>(v); + stack_.back() = f(*v); + } + + // Executes x = pop(); push(f(pop(), x)). + template <typename T1, typename T2, typename R> + void Binop(std::function<R(T1, T2)> f) { + if (stack_.size() < 2) { + throw std::runtime_error("stack underflow during binary operation"); + } + int target = stack_.size() - 2; + T1* one = absl::get_if<T1>(&stack_[target]); + T2* two = absl::get_if<T2>(&stack_.back()); + RequireType<T1>(one); + RequireType<T2>(two); + stack_[target] = f(*one, *two); + stack_.pop_back(); + } + + // Convenience specializations for Unop and Binop over integers. + void UnopN(std::function<int(int)> f) { Unop<int, int>(f); } + void BinopN(std::function<int(int, int)> f) { Binop<int, int, int>(f); } + + template <typename T> + void EmitPopped(absl::string_view format) { + if (format[0] == ':') { + // The colon is used by to disambiguate parsing. It isn't interesting to + // absl::FormatUntyped. + format = format.substr(1); + } + + std::string out; + auto* param = absl::get_if<T>(&stack_.back()); + RequireType<T>(param); + if (!absl::FormatUntyped(&out, + absl::UntypedFormatSpec(absl::StrCat("%", format)), + {absl::FormatArg(*param)})) { + throw std::logic_error( + absl::StrCat("absl::FormatUntyped failed with format ", format)); + } + Bytes(out); + stack_.pop_back(); + } + + void ExpectDelay(absl::Duration delay) noexcept { + result_.cost += absl::ToDoubleMilliseconds(delay) * chars_per_ms_; + } + + void Delay(absl::Duration delay) noexcept { + if (!pending_bytes_.empty()) { + Flush(); + } + pending_delay_.Increase(delay); + ExpectDelay(delay); + } + + void Bytes(absl::string_view bytes) noexcept { + pending_bytes_.Append(bytes); + result_.cost += bytes.size(); + } + + void Flush() noexcept { + if (!pending_delay_.zero()) { + result_.terms.push_back( + std::make_shared<EmitDelay>(std::move(pending_delay_))); + }; + if (!pending_bytes_.empty()) { + result_.terms.push_back( + std::make_shared<EmitBytes>(std::move(pending_bytes_))); + }; + } + + InterpretStringCapabilityInput input_; + double chars_per_ms_; + + std::vector<StringCapabilityParameter> stack_; + absl::flat_hash_map<char, StringCapabilityParameter> environment_; + + EmitDelay pending_delay_; + EmitBytes pending_bytes_; + + InterpretStringCapabilityResult result_; +}; + +} // namespace + +InterpretStringCapabilityResult InterpretStringCapability( + const InterpretStringCapabilityInput& input) { + Interpreter interp(input); + interp.Run(); + return interp.result(); +} + +} // namespace goldfishterm_internal diff --git a/goldfishterm/internal/string_capability_test.cc b/goldfishterm/internal/string_capability_test.cc new file mode 100644 index 0000000..a821551 --- /dev/null +++ b/goldfishterm/internal/string_capability_test.cc @@ -0,0 +1,372 @@ +// 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 "goldfishterm/internal/string_capability.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <string.h> + +#include <ostream> +#include <stdexcept> +#include <string> +#include <type_traits> +#include <utility> +#include <vector> + +#include "goldfishterm/internal/emit.h" +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/time/time.h" +#include "third_party/abseil/absl/types/optional.h" + +namespace goldfishterm_internal { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Pointee; +using ::testing::Property; +using ::testing::StrEq; +using ::testing::WhenDynamicCastTo; + +MATCHER_P(PointsToEmitBytes, s, + absl::StrCat("points to EmitBytes('", s, "')")) { + const char* start; + int length; + if constexpr (std::is_same_v<decltype(s), const char* const>) { + start = s; + length = strlen(s); + } else { + start = s.data(); + length = s.size(); + } + return ExplainMatchResult(Pointee(WhenDynamicCastTo<const EmitBytes&>( + EmitBytes(absl::string_view(start, length)))), + arg, result_listener); +} + +MATCHER_P(PointsToEmitDelay, t, + absl::StrCat("points to EmitDelay('", absl::FormatDuration(t), + "')")) { + return ExplainMatchResult( + Pointee(WhenDynamicCastTo<const EmitDelay&>(EmitDelay(t))), arg, + result_listener); +} + +InterpretStringCapabilityResult Interpret( + std::string text, std::vector<StringCapabilityParameter> parameters = {}) { + InterpretStringCapabilityInput input; + input.capability = std::move(text); + input.parameters = std::move(parameters); + return InterpretStringCapability(input); +} + +TEST(InterpretStringCapabilityTest, PadInteger) { + InterpretStringCapabilityInput input; + input.capability = "$<40>"; + input.baud = 8'000; // 1 character every millisecond + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, + ElementsAre(PointsToEmitBytes(std::string(40, '\0')))); + EXPECT_EQ(result.cost, 40); +} + +TEST(InterpretStringCapabilityTest, PadFractional) { + InterpretStringCapabilityInput input; + input.capability = "$<.9>"; + input.baud = 80'000; // 10 characters every millisecond + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, + ElementsAre(PointsToEmitBytes(std::string(9, '\0')))); + EXPECT_EQ(result.cost, 9); +} + +TEST(InterpretStringCapabilityTest, PadIgnoredIfMultipleSignificantFigures) { + EXPECT_THAT(Interpret("$<1.1>").terms, + ElementsAre(PointsToEmitBytes("$<1.1>"))); +} + +TEST(InterpretStringCapabilityTest, PerLinePad) { + InterpretStringCapabilityInput input; + input.capability = "$<4*>"; + input.lines_affected = 10; + input.baud = 8'000; + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, + ElementsAre(PointsToEmitBytes(std::string(40, '\0')))); + EXPECT_EQ(result.cost, 40); +} + +TEST(InterpretStringCapabilityTest, PadIgnoredIfTerminalTooSlow) { + InterpretStringCapabilityInput input; + input.capability = "$<40>"; + input.padding_baud_rate = 115200; + input.baud = 9600; + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, IsEmpty()); + EXPECT_EQ(result.cost, 0); +} + +TEST(InterpretStringCapabilityTest, PadWithWeirdCharacter) { + InterpretStringCapabilityInput input; + input.capability = "$<.1>"; + input.pad_char = 'a'; + input.baud = 9600; + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("a"))); + EXPECT_EQ(result.cost, 1); +} + +TEST(InterpretStringCapabilityTest, SleepIfPadUnsupported) { + InterpretStringCapabilityInput input; + input.capability = "$<40>"; + input.pad_char = absl::nullopt; + input.baud = 16'000; + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, + ElementsAre(PointsToEmitDelay(absl::Milliseconds(40)))); + EXPECT_EQ(result.cost, 80); // the equivalent of 80 characters +} + +TEST(InterpretStringCapabilityTest, PadWith8N1) { + InterpretStringCapabilityInput input; + input.capability = "$<40>"; + input.baud = 10'000; // 1 character every millisecond using + input.extra_bits_per_character = 2; // one start and one stop bit + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, + ElementsAre(PointsToEmitBytes(std::string(40, '\0')))); + EXPECT_EQ(result.cost, 40); +} + +TEST(InterpretStringCapabilityTest, PadIgnoredIfXonXoffAvailable) { + InterpretStringCapabilityInput input; + input.capability = "$<40>"; + input.has_xon_xoff = true; + input.baud = 8'000; + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, IsEmpty()); + EXPECT_EQ(result.cost, 40); +} + +TEST(InterpretStringCapabilityTest, MandatoryPadSentEvenIfXonXoffAvailable) { + InterpretStringCapabilityInput input; + input.capability = "$<40/>"; + input.has_xon_xoff = true; + input.baud = 8'000; + InterpretStringCapabilityResult result = InterpretStringCapability(input); + EXPECT_THAT(result.terms, + ElementsAre(PointsToEmitBytes(std::string(40, '\0')))); + EXPECT_EQ(result.cost, 40); +} + +TEST(InterpretStringCapabilityTest, Empty) { + InterpretStringCapabilityResult result = Interpret(""); + EXPECT_THAT(result.terms, IsEmpty()); + EXPECT_EQ(result.cost, 0); +} + +TEST(InterpretStringCapabilityTest, EmitLiteralPercent) { + InterpretStringCapabilityResult result = Interpret("%%"); + EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("%"))); + EXPECT_EQ(result.cost, 1); +} + +TEST(InterpretStringCapabilityTest, EmitChar) { + InterpretStringCapabilityResult result = Interpret("a"); + EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("a"))); + EXPECT_EQ(result.cost, 1); +} + +TEST(InterpretStringCapabilityTest, EmitText) { + InterpretStringCapabilityResult result = Interpret("hi"); + EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("hi"))); + EXPECT_EQ(result.cost, 2); +} + +TEST(InterpretStringCapabilityTest, EmitNonprinting) { + InterpretStringCapabilityResult result = Interpret("\r\n"); + EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("\r\n"))); + EXPECT_EQ(result.cost, 2); +} + +TEST(InterpretStringCapabilityTest, PushParameter) { + EXPECT_THAT(Interpret("%p2%d", {42, 96}).terms, + ElementsAre(PointsToEmitBytes("96"))); +} + +TEST(InterpretStringCapabilityTest, PopNumber) { + EXPECT_THAT(Interpret("%p1%:-10.5o", {42}).terms, + ElementsAre(PointsToEmitBytes("00052 "))); +} + +TEST(InterpretStringCapabilityTest, PopString) { + EXPECT_THAT(Interpret("%p1%:-10s", {"foo"}).terms, + ElementsAre(PointsToEmitBytes("foo "))); +} + +TEST(InterpretStringCapabilityTest, PopChar) { + EXPECT_THAT(Interpret("%p1%:-10c", {'x'}).terms, + ElementsAre(PointsToEmitBytes("x "))); +} + +TEST(InterpretStringCapabilityTest, StoreRecall) { + EXPECT_THAT(Interpret("%p1%Pa%p2%Pb%gb%s,%ga%d", {1, "foo"}).terms, + ElementsAre(PointsToEmitBytes("foo,1"))); +} + +TEST(InterpretStringCapabilityTest, PushChar) { + EXPECT_THAT(Interpret("%'x'%'y'%c%c").terms, + ElementsAre(PointsToEmitBytes("yx"))); +} + +TEST(InterpretStringCapabilityTest, PushNumber) { + EXPECT_THAT(Interpret("%{123}%d").terms, + ElementsAre(PointsToEmitBytes("123"))); +} + +TEST(InterpretStringCapabilityTest, Strlen) { + EXPECT_THAT(Interpret("%p1%l%d", {"foobar"}).terms, + ElementsAre(PointsToEmitBytes("6"))); +} + +TEST(InterpretStringCapabilityTest, Add) { + EXPECT_THAT(Interpret("%{5}%{2}%+%d").terms, + ElementsAre(PointsToEmitBytes("7"))); +} + +TEST(InterpretStringCapabilityTest, Subtract) { + EXPECT_THAT(Interpret("%{5}%{2}%-%d").terms, + ElementsAre(PointsToEmitBytes("3"))); +} + +TEST(InterpretStringCapabilityTest, Multiply) { + EXPECT_THAT(Interpret("%{5}%{2}%*%d").terms, + ElementsAre(PointsToEmitBytes("10"))); +} + +TEST(InterpretStringCapabilityTest, Divide) { + EXPECT_THAT(Interpret("%{5}%{2}%/%d").terms, + ElementsAre(PointsToEmitBytes("2"))); +} + +TEST(InterpretStringCapabilityTest, Modulo) { + EXPECT_THAT(Interpret("%{5}%{2}%m%d").terms, + ElementsAre(PointsToEmitBytes("1"))); +} + +TEST(InterpretStringCapabilityTest, BitwiseAnd) { + EXPECT_THAT(Interpret("%{3}%{15}%&%d").terms, + ElementsAre(PointsToEmitBytes("3"))); +} + +TEST(InterpretStringCapabilityTest, BitwiseOr) { + EXPECT_THAT(Interpret("%{2}%{1}%|%d").terms, + ElementsAre(PointsToEmitBytes("3"))); +} + +TEST(InterpretStringCapabilityTest, BitwiseXor) { + EXPECT_THAT(Interpret("%{15}%{10}%^%d").terms, + ElementsAre(PointsToEmitBytes("5"))); +} + +TEST(InterpretStringCapabilityTest, Equals) { + EXPECT_THAT(Interpret("%{15}%{15}%=%d").terms, + ElementsAre(PointsToEmitBytes("1"))); + EXPECT_THAT(Interpret("%{15}%{14}%=%d").terms, + ElementsAre(PointsToEmitBytes("0"))); +} + +TEST(InterpretStringCapabilityTest, LessThan) { + EXPECT_THAT(Interpret("%{2}%{3}%<%d").terms, + ElementsAre(PointsToEmitBytes("1"))); + EXPECT_THAT(Interpret("%{2}%{2}%<%d").terms, + ElementsAre(PointsToEmitBytes("0"))); +} + +TEST(InterpretStringCapabilityTest, GreaterThan) { + EXPECT_THAT(Interpret("%{3}%{2}%>%d").terms, + ElementsAre(PointsToEmitBytes("1"))); + EXPECT_THAT(Interpret("%{2}%{2}%>%d").terms, + ElementsAre(PointsToEmitBytes("0"))); +} + +TEST(InterpretStringCapabilityTest, LogicalAnd) { + EXPECT_THAT(Interpret("%{0}%{0}%A%d").terms, + ElementsAre(PointsToEmitBytes("0"))); + EXPECT_THAT(Interpret("%{0}%{1}%A%d").terms, + ElementsAre(PointsToEmitBytes("0"))); + EXPECT_THAT(Interpret("%{1}%{0}%A%d").terms, + ElementsAre(PointsToEmitBytes("0"))); + EXPECT_THAT(Interpret("%{1}%{1}%A%d").terms, + ElementsAre(PointsToEmitBytes("1"))); +} + +TEST(InterpretStringCapabilityTest, LogicalOr) { + EXPECT_THAT(Interpret("%{0}%{0}%O%d").terms, + ElementsAre(PointsToEmitBytes("0"))); + EXPECT_THAT(Interpret("%{0}%{1}%O%d").terms, + ElementsAre(PointsToEmitBytes("1"))); + EXPECT_THAT(Interpret("%{1}%{0}%O%d").terms, + ElementsAre(PointsToEmitBytes("1"))); + EXPECT_THAT(Interpret("%{1}%{1}%O%d").terms, + ElementsAre(PointsToEmitBytes("1"))); +} + +TEST(InterpretStringCapabilityTest, LogicalNot) { + EXPECT_THAT(Interpret("%{0}%!%d").terms, ElementsAre(PointsToEmitBytes("1"))); + EXPECT_THAT(Interpret("%{5}%!%d").terms, ElementsAre(PointsToEmitBytes("0"))); +} + +TEST(InterpretStringCapabilityTest, BitwiseNot) { + EXPECT_THAT(Interpret("%{8}%~%d").terms, + ElementsAre(PointsToEmitBytes("-9"))); +} + +TEST(InterpretStringCapabilityTest, IncrementParameters) { + EXPECT_THAT(Interpret("%i%p1%d,%p2%d", {6, 8}).terms, + ElementsAre(PointsToEmitBytes("7,9"))); +} + +TEST(InterpretStringCapabilityTest, IfThen) { + EXPECT_THAT(Interpret("%?%{0}%ttrue%;").terms, IsEmpty()); + EXPECT_THAT(Interpret("%?%{1}%ttrue%;").terms, + ElementsAre(PointsToEmitBytes("true"))); +} + +TEST(InterpretStringCapabilityTest, IfThenElse) { + EXPECT_THAT(Interpret("%?%{0}%ttrue%efalse%;").terms, + ElementsAre(PointsToEmitBytes("false"))); + EXPECT_THAT(Interpret("%?%{1}%ttrue%efalse%;").terms, + ElementsAre(PointsToEmitBytes("true"))); +} + +TEST(InterpretStringCapabilityTest, IfThenElseThen) { + EXPECT_THAT(Interpret("%?%{0}%t!%e%{1}%tgood%;").terms, + ElementsAre(PointsToEmitBytes("good"))); +} + +TEST(InterpretStringCapabilityTest, IfThenElseThenElse) { + EXPECT_THAT(Interpret("%?%{0}%t!%e%{0}%t!%egood%;").terms, + ElementsAre(PointsToEmitBytes("good"))); +} + +TEST(InterpretStringCapabilityTest, IfThenElseThenElseShortCircuit) { + EXPECT_THAT(Interpret("%?%{1}%tgood%e%{0}%t!%e!%;").terms, + ElementsAre(PointsToEmitBytes("good"))); +} + +} // namespace +} // namespace goldfishterm_internal diff --git a/goldfishterm/simple.cc b/goldfishterm/simple.cc new file mode 100644 index 0000000..9c4c9a9 --- /dev/null +++ b/goldfishterm/simple.cc @@ -0,0 +1,74 @@ +// 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 "goldfishterm/simple.h" + +#include <stdlib.h> +#include <termios.h> +#include <unistd.h> + +#include <stdexcept> +#include <vector> + +#include "goldfishterm/internal/emit.h" +#include "goldfishterm/terminfo.h" +#include "third_party/abseil/absl/strings/string_view.h" + +namespace goldfishterm { + +namespace { + +using ::goldfishterm_internal::InterpretStringCapability; +using ::goldfishterm_internal::InterpretStringCapabilityInput; + +termios StdoutTermios() { + termios tty; + if (tcgetattr(STDOUT_FILENO, &tty) < 0) { + throw std::system_error(errno, std::system_category(), "tcgetattr"); + } + return tty; +} + +absl::string_view TerminalNameFromEnvironment() { + const char* name = getenv("TERM"); + if (name == nullptr) { + throw std::runtime_error("TERM unset"); + } + return name; +} + +} // namespace + +SimpleTerminalOutput::SimpleTerminalOutput() + : SimpleTerminalOutput(StdoutTermios()) {} + +SimpleTerminalOutput::SimpleTerminalOutput(const termios& tty) + : terminfo_( + TerminfoEntry::FromSystemDatabase(TerminalNameFromEnvironment())) { + baud_ = cfgetospeed(&tty); +} + +void SimpleTerminalOutput::Emit( + StringCapability cap, + std::vector<goldfishterm_internal::StringCapabilityParameter> parameters) { + goldfishterm_internal::Emit( + InterpretStringCapability( + InterpretStringCapabilityInput(terminfo_, terminfo_.get(cap).value(), + std::move(parameters), /*baud=*/baud_, + /*lines_affected=*/1)) + .terms, + std::cout); +} + +} // namespace goldfishterm diff --git a/goldfishterm/simple.h b/goldfishterm/simple.h new file mode 100644 index 0000000..3f5cc9f --- /dev/null +++ b/goldfishterm/simple.h @@ -0,0 +1,71 @@ +// 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_GOLDFISHTERM_SIMPLE_H_ +#define EC_GOLDFISHTERM_SIMPLE_H_ + +#include <termios.h> + +#include <iostream> +#include <vector> + +#include "goldfishterm/internal/string_capability.h" +#include "goldfishterm/terminfo.h" +#include "third_party/abseil/absl/strings/string_view.h" + +namespace goldfishterm { + +// Looks up escape sequences for the terminal described in the TERM environment +// variable, and allows sending those escape sequences to standard output. +// +// This class is thread-safe, provided you don't mutate the TERM environment +// variable while its constructor is running. +class SimpleTerminalOutput final { + public: + explicit SimpleTerminalOutput(); + explicit SimpleTerminalOutput(const termios&); + + SimpleTerminalOutput(const SimpleTerminalOutput&) noexcept = default; + SimpleTerminalOutput& operator=(const SimpleTerminalOutput&) noexcept = + default; + SimpleTerminalOutput(SimpleTerminalOutput&&) noexcept = default; + SimpleTerminalOutput& operator=(SimpleTerminalOutput&&) noexcept = default; + + void Write(absl::string_view s) { std::cout.write(s.data(), s.size()); } + + void Flush() { std::cout.flush(); } + + // Rings the bell. On some terminals, this may flash the screen instead. + void Beep() { Emit(StringCapability::kBell); } + + // Moves the cursor to the beginning of the current line. + void BeginningOfLine() { Emit(StringCapability::kCarriageReturn); } + + // Clears from the cursor to the end of the current line. + void ClearToEndOfLine() { Emit(StringCapability::kClrEol); } + + // Moves the cursor down one line. + void CursorDown() { Emit(StringCapability::kCursorDown); } + + private: + void Emit(StringCapability, + std::vector<goldfishterm_internal::StringCapabilityParameter> = {}); + + TerminfoEntry terminfo_; + int baud_; +}; + +} // namespace goldfishterm + +#endif // EC_GOLDFISHTERM_SIMPLE_H_ diff --git a/goldfishterm/terminfo.cc b/goldfishterm/terminfo.cc new file mode 100644 index 0000000..b58fb5b --- /dev/null +++ b/goldfishterm/terminfo.cc @@ -0,0 +1,325 @@ +// 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 "goldfishterm/terminfo.h" + +#include <byteswap.h> +#include <stdint.h> +#include <stdlib.h> + +#include <fstream> +#include <istream> +#include <stdexcept> +#include <string> +#include <vector> + +#include "third_party/abseil/absl/base/config.h" +#include "third_party/abseil/absl/meta/type_traits.h" +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/strings/str_format.h" +#include "third_party/abseil/absl/strings/str_split.h" +#include "third_party/abseil/absl/types/optional.h" + +namespace goldfishterm { + +namespace { + +constexpr int16_t kTerminfoMagic = 0432; +constexpr int16_t kTerminfoExtendedNumberFormatMagic = 01036; +constexpr char kSystemTerminfoDirectory[] = "/etc/terminfo"; + +template <typename T> +constexpr absl::underlying_type_t<T> FromEnum(T x) { + return static_cast<absl::underlying_type_t<T>>(x); +} + +std::string GetEnv(const char* var) noexcept { + const char* val = getenv(var); + if (val == nullptr) { + return ""; + } + return val; +} + +// Reads a potentially unaligned little-endian int16_t. +int16_t ReadShort(std::istream& in) { + // TODO(bbarenblat@gmail.com): Do we need to support anything else here (maybe + // PDP-11-endian)? +#if !defined(ABSL_IS_LITTLE_ENDIAN) && !defined(ABSL_IS_BIG_ENDIAN) +#error Neither ABSL_IS_LITTLE_ENDIAN nor ABSL_IS_BIG_ENDIAN is defined +#endif + + int16_t n; + in.read(reinterpret_cast<char*>(&n), sizeof(int16_t)); +#ifdef ABSL_IS_BIG_ENDIAN + n = bswap_16(n); +#endif + return n; +} + +// Reads a potentially unaligned little-endian int32_t. +int32_t ReadInt(std::istream& in) { + // TODO(bbarenblat@gmail.com): Do we need to support anything else here (maybe + // PDP-11-endian)? +#if !defined(ABSL_IS_LITTLE_ENDIAN) && !defined(ABSL_IS_BIG_ENDIAN) +#error Neither ABSL_IS_LITTLE_ENDIAN nor ABSL_IS_BIG_ENDIAN is defined +#endif + + int32_t n; + in.read(reinterpret_cast<char*>(&n), sizeof(int32_t)); +#ifdef ABSL_IS_BIG_ENDIAN + n = bswap_32(n); +#endif + return n; +} + +int16_t ReadNonnegativeShort(const char* error_message, std::istream& in) { + int16_t n = ReadShort(in); + if (n < 0) { + throw std::runtime_error(error_message); + } + return n; +} + +void RequireValidNumber(const char* error_message, int n) { + // -1 and -2 are reserved to mean "not present" and "canceled", respectively. + if (n < -2) { + throw std::runtime_error(absl::StrCat(error_message, " ", n)); + } +} + +struct Header final { + bool extended_number_format; + int names_bytes; + int booleans_bytes; + int numbers_count; + int strings_offsets_count; + int strings_bytes; + + Header(std::istream& description) { + switch (ReadShort(description)) { + case kTerminfoMagic: + extended_number_format = false; + break; + case kTerminfoExtendedNumberFormatMagic: + extended_number_format = true; + break; + default: + throw std::runtime_error("bad magic"); + } + names_bytes = ReadNonnegativeShort( + "terminal names section cannot have negative length", description); + booleans_bytes = ReadNonnegativeShort( + "boolean flags section cannot have negative length", description); + numbers_count = ReadNonnegativeShort( + "numbers section cannot have negative length", description); + strings_offsets_count = ReadNonnegativeShort( + "strings section cannot have negative numbers of entries", description); + strings_bytes = ReadNonnegativeShort( + "strings table cannot have negative length", description); + } +}; + +std::vector<std::string> ReadTerminalNames(int bytes, std::istream& in) { + assert(bytes > 0); + std::string names(bytes - 1, '\0'); + in.read(names.data(), names.size()); + // Discard the null at the end of the names list. + if (in.get() != '\0') { + throw std::runtime_error("terminal names were not null-terminated"); + } + return absl::StrSplit(names, '|'); +} + +std::vector<int8_t> ReadBooleanFlags(int bytes, std::istream& in) { + std::vector<int8_t> booleans(bytes); + in.read(reinterpret_cast<char*>(booleans.data()), booleans.size()); + for (int8_t b : booleans) { + RequireValidNumber("invalid boolean capability", b); + } + return booleans; +} + +std::vector<int32_t> ReadNumbers(bool extended_number_format, int count, + std::istream& in) { + std::vector<int32_t> numbers; + numbers.reserve(count); + for (int i = 0; i < count; ++i) { + int32_t n; + if (extended_number_format) { + n = ReadInt(in); + } else { + n = ReadShort(in); + } + RequireValidNumber("invalid numeric capability", n); + numbers.push_back(n); + } + return numbers; +} + +std::vector<absl::optional<std::string>> ReadStrings(int offsets_count, + int bytes, + std::istream& in) { + std::vector<int16_t> offsets; + offsets.reserve(offsets_count); + for (int i = 0; i < offsets_count; ++i) { + int16_t n = ReadShort(in); + RequireValidNumber("invalid string offset", n); + offsets.push_back(n); + } + + // Read the string table, including the trailing null (so we can easily find + // the end of each capability). + std::string data(bytes, '\0'); + in.read(data.data(), data.size()); + + std::vector<absl::optional<std::string>> result; + for (int16_t offset : offsets) { + if (offset < 0) { + result.push_back(absl::nullopt); + } else { + result.push_back(std::string(data.data() + offset)); + } + } + return result; +} + +std::vector<std::string> DatabasePaths() noexcept { + // If TERMINFO is set, only look there. + if (std::string terminfo = GetEnv("TERMINFO"); !terminfo.empty()) { + return {terminfo}; + } + + std::vector<std::string> paths; + + // Start by looking in HOME. + if (std::string home = GetEnv("HOME"); !home.empty()) { + paths.push_back(absl::StrCat(home, "/.terminfo")); + } + + // Next, look in TERMINFO_DIRS. + if (std::string terminfo_dirs = GetEnv("TERMINFO_DIRS"); + !terminfo_dirs.empty()) { + for (absl::string_view dir : absl::StrSplit(terminfo_dirs, ':')) { + if (dir.empty()) { + paths.push_back(kSystemTerminfoDirectory); + } else { + paths.push_back(std::string(dir)); + } + } + } + + // Finally, look in system locations. + paths.push_back(kSystemTerminfoDirectory); + paths.push_back("/lib/terminfo"); + paths.push_back("/usr/share/terminfo"); + + return paths; +} + +std::ifstream OpenDescriptionForTerminal(absl::string_view db_path, + absl::string_view name) { + // The path to the terminal entry inside the database takes the general form + // `c/name`, where c is the first character of the name (on file systems that + // are case-sensitive) and the hex encoding of the name (on file systems that + // are not). + + if (name.empty()) { + throw std::invalid_argument("empty terminal name"); + } + + std::ifstream entry(absl::StrFormat("%s/%02x/%s", db_path, name[0], name)); + if (!entry.fail()) { + return entry; + } + + entry = std::ifstream(absl::StrFormat("%s/%c/%s", db_path, name[0], name)); + if (!entry.fail()) { + return entry; + } + + throw std::runtime_error("terminfo entry not found"); +} + +} // namespace + +TerminfoEntry TerminfoEntry::FromSystemDatabase( + absl::string_view terminal_name) { + for (const auto& path : DatabasePaths()) { + try { + std::ifstream binary = OpenDescriptionForTerminal(path, terminal_name); + return TerminfoEntry(binary); + } catch (const std::runtime_error&) { + continue; + } + } + throw std::runtime_error("no valid system entry by that name"); +} + +TerminfoEntry::TerminfoEntry(std::istream& description) { + description.exceptions(std::istream::badbit | std::istream::failbit); + + Header header(description); + if (header.names_bytes > 0) { + names_ = ReadTerminalNames(header.names_bytes, description); + } + booleans_ = ReadBooleanFlags(header.booleans_bytes, description); + + // Discard the padding byte between the boolean flags section and the number + // section, if it exists. + if ((header.names_bytes + header.booleans_bytes) % 2 == 1 && + (header.numbers_count != 0 || header.strings_offsets_count != 0 || + header.strings_bytes != 0) && + description.get() != '\0') { + throw std::runtime_error("padding byte was nonnull"); + } + + numbers_ = ReadNumbers(header.extended_number_format, header.numbers_count, + description); + strings_ = ReadStrings(header.strings_offsets_count, header.strings_bytes, + description); +} + +bool TerminfoEntry::get(BooleanCapability cap) const noexcept { + try { + return booleans_.at(FromEnum(cap)) > 0; + } catch (const std::out_of_range&) { + return false; + } +} + +absl::optional<int> TerminfoEntry::get(NumericCapability cap) const noexcept { + try { + int r = numbers_.at(FromEnum(cap)); + if (r >= 0) { + return r; + } else { + return absl::nullopt; + } + } catch (const std::out_of_range&) { + return absl::nullopt; + } +} + +const absl::optional<std::string>& TerminfoEntry::get( + StringCapability cap) const noexcept { + try { + return strings_.at(FromEnum(cap)); + } catch (const std::out_of_range&) { + static const absl::optional<std::string> kNone = absl::nullopt; + return kNone; + } +} + +} // namespace goldfishterm diff --git a/goldfishterm/terminfo.h b/goldfishterm/terminfo.h new file mode 100644 index 0000000..9452468 --- /dev/null +++ b/goldfishterm/terminfo.h @@ -0,0 +1,544 @@ +// 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. + +// Terminfo entries and the terminfo database. + +#ifndef EC_GOLDFISHTERM_TERMINFO_H_ +#define EC_GOLDFISHTERM_TERMINFO_H_ + +#include <stdint.h> + +#include <istream> +#include <string> +#include <vector> + +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/types/optional.h" + +namespace goldfishterm { + +// Boolean terminal capabilities. See terminfo(5) for full documentation. +enum class BooleanCapability { + kAutoLeftMargin = 0, + kAutoRightMargin = 1, + kNoEscCtlc = 2, + kCeolStandoutGlitch = 3, + kEatNewlineGlitch = 4, + kEraseOverstrike = 5, + kGenericType = 6, + kHardCopy = 7, + kHasMetaKey = 8, + kHasStatusLine = 9, + kInsertNullGlitch = 10, + kMemoryAbove = 11, + kMemoryBelow = 12, + kMoveInsertMode = 13, + kMoveStandoutMode = 14, + kOverStrike = 15, + kStatusLineEscOk = 16, + kDestTabsMagicSmso = 17, + kTildeGlitch = 18, + kTransparentUnderline = 19, + kXonXoff = 20, + kNeedsXonXoff = 21, + kPrtrSilent = 22, + kHardCursor = 23, + kNonRevRmcup = 24, + kNoPadChar = 25, + kNonDestScrollRegion = 26, + kCanChange = 27, + kBackColorErase = 28, + kHueLightnessSaturation = 29, + kColAddrGlitch = 30, + kCrCancelsMicroMode = 31, + kHasPrintWheel = 32, + kRowAddrGlitch = 33, + kSemiAutoRightMargin = 34, + kCpiChangesRes = 35, + kLpiChangesRes = 36, +}; + +// Numeric terminal capabilities. See terminfo(5) for full documentation. +enum class NumericCapability { + kColumns = 0, + kInitTabs = 1, + kLines = 2, + kLinesOfMemory = 3, + kMagicCookieGlitch = 4, + kPaddingBaudRate = 5, + kVirtualTerminal = 6, + kWidthStatusLine = 7, + kNumLabels = 8, + kLabelHeight = 9, + kLabelWidth = 10, + kMaxAttributes = 11, + kMaximumWindows = 12, + kMaxColors = 13, + kMaxPairs = 14, + kNoColorVideo = 15, + kBufferCapacity = 16, + kDotVertSpacing = 17, + kDotHorzSpacing = 18, + kMaxMicroAddress = 19, + kMaxMicroJump = 20, + kMicroColSize = 21, + kMicroLineSize = 22, + kNumberOfPins = 23, + kOutputResChar = 24, + kOutputResLine = 25, + kOutputResHorzInch = 26, + kOutputResVertInch = 27, + kPrintRate = 28, + kWideCharSize = 29, + kButtons = 30, + kBitImageEntwining = 31, + kBitImageType = 32, +}; + +// String terminal capabilities. See terminfo(5) for full documentation. +enum class StringCapability { + kBackTab = 0, + kBell = 1, + kCarriageReturn = 2, + kChangeScrollRegion = 3, + kClearAllTabs = 4, + kClearScreen = 5, + kClrEol = 6, + kClrEos = 7, + kColumnAddress = 8, + kCommandCharacter = 9, + kCursorAddress = 10, + kCursorDown = 11, + kCursorHome = 12, + kCursorInvisible = 13, + kCursorLeft = 14, + kCursorMemAddress = 15, + kCursorNormal = 16, + kCursorRight = 17, + kCursorToLl = 18, + kCursorUp = 19, + kCursorVisible = 20, + kDeleteCharacter = 21, + kDeleteLine = 22, + kDisStatusLine = 23, + kDownHalfLine = 24, + kEnterAltCharsetMode = 25, + kEnterBlinkMode = 26, + kEnterBoldMode = 27, + kEnterCaMode = 28, + kEnterDeleteMode = 29, + kEnterDimMode = 30, + kEnterInsertMode = 31, + kEnterSecureMode = 32, + kEnterProtectedMode = 33, + kEnterReverseMode = 34, + kEnterStandoutMode = 35, + kEnterUnderlineMode = 36, + kEraseChars = 37, + kExitAltCharsetMode = 38, + kExitAttributeMode = 39, + kExitCaMode = 40, + kExitDeleteMode = 41, + kExitInsertMode = 42, + kExitStandoutMode = 43, + kExitUnderlineMode = 44, + kFlashScreen = 45, + kFormFeed = 46, + kFromStatusLine = 47, + kInit1string = 48, + kInit2string = 49, + kInit3string = 50, + kInitFile = 51, + kInsertCharacter = 52, + kInsertLine = 53, + kInsertPadding = 54, + kKeyBackspace = 55, + kKeyCatab = 56, + kKeyClear = 57, + kKeyCtab = 58, + kKeyDc = 59, + kKeyDl = 60, + kKeyDown = 61, + kKeyEic = 62, + kKeyEol = 63, + kKeyEos = 64, + kKeyF0 = 65, + kKeyF1 = 66, + kKeyF10 = 67, + kKeyF2 = 68, + kKeyF3 = 69, + kKeyF4 = 70, + kKeyF5 = 71, + kKeyF6 = 72, + kKeyF7 = 73, + kKeyF8 = 74, + kKeyF9 = 75, + kKeyHome = 76, + kKeyIc = 77, + kKeyIl = 78, + kKeyLeft = 79, + kKeyLl = 80, + kKeyNpage = 81, + kKeyPpage = 82, + kKeyRight = 83, + kKeySf = 84, + kKeySr = 85, + kKeyStab = 86, + kKeyUp = 87, + kKeypadLocal = 88, + kKeypadXmit = 89, + kLabF0 = 90, + kLabF1 = 91, + kLabF10 = 92, + kLabF2 = 93, + kLabF3 = 94, + kLabF4 = 95, + kLabF5 = 96, + kLabF6 = 97, + kLabF7 = 98, + kLabF8 = 99, + kLabF9 = 100, + kMetaOff = 101, + kMetaOn = 102, + kNewline = 103, + kPadChar = 104, + kParmDch = 105, + kParmDeleteLine = 106, + kParmDownCursor = 107, + kParmIch = 108, + kParmIndex = 109, + kParmInsertLine = 110, + kParmLeftCursor = 111, + kParmRightCursor = 112, + kParmRindex = 113, + kParmUpCursor = 114, + kPkeyKey = 115, + kPkeyLocal = 116, + kPkeyXmit = 117, + kPrintScreen = 118, + kPrtrOff = 119, + kPrtrOn = 120, + kRepeatChar = 121, + kReset1string = 122, + kReset2string = 123, + kReset3string = 124, + kResetFile = 125, + kRestoreCursor = 126, + kRowAddress = 127, + kSaveCursor = 128, + kScrollForward = 129, + kScrollReverse = 130, + kSetAttributes = 131, + kSetTab = 132, + kSetWindow = 133, + kTab = 134, + kToStatusLine = 135, + kUnderlineChar = 136, + kUpHalfLine = 137, + kInitProg = 138, + kKeyA1 = 139, + kKeyA3 = 140, + kKeyB2 = 141, + kKeyC1 = 142, + kKeyC3 = 143, + kPrtrNon = 144, + kCharPadding = 145, + kAcsChars = 146, + kPlabNorm = 147, + kKeyBtab = 148, + kEnterXonMode = 149, + kExitXonMode = 150, + kEnterAmMode = 151, + kExitAmMode = 152, + kXonCharacter = 153, + kXoffCharacter = 154, + kEnaAcs = 155, + kLabelOn = 156, + kLabelOff = 157, + kKeyBeg = 158, + kKeyCancel = 159, + kKeyClose = 160, + kKeyCommand = 161, + kKeyCopy = 162, + kKeyCreate = 163, + kKeyEnd = 164, + kKeyEnter = 165, + kKeyExit = 166, + kKeyFind = 167, + kKeyHelp = 168, + kKeyMark = 169, + kKeyMessage = 170, + kKeyMove = 171, + kKeyNext = 172, + kKeyOpen = 173, + kKeyOptions = 174, + kKeyPrevious = 175, + kKeyPrint = 176, + kKeyRedo = 177, + kKeyReference = 178, + kKeyRefresh = 179, + kKeyReplace = 180, + kKeyRestart = 181, + kKeyResume = 182, + kKeySave = 183, + kKeySuspend = 184, + kKeyUndo = 185, + kKeySbeg = 186, + kKeyScancel = 187, + kKeyScommand = 188, + kKeyScopy = 189, + kKeyScreate = 190, + kKeySdc = 191, + kKeySdl = 192, + kKeySelect = 193, + kKeySend = 194, + kKeySeol = 195, + kKeySexit = 196, + kKeySfind = 197, + kKeyShelp = 198, + kKeyShome = 199, + kKeySic = 200, + kKeySleft = 201, + kKeySmessage = 202, + kKeySmove = 203, + kKeySnext = 204, + kKeySoptions = 205, + kKeySprevious = 206, + kKeySprint = 207, + kKeySredo = 208, + kKeySreplace = 209, + kKeySright = 210, + kKeySrsume = 211, + kKeySsave = 212, + kKeySsuspend = 213, + kKeySundo = 214, + kReqForInput = 215, + kKeyF11 = 216, + kKeyF12 = 217, + kKeyF13 = 218, + kKeyF14 = 219, + kKeyF15 = 220, + kKeyF16 = 221, + kKeyF17 = 222, + kKeyF18 = 223, + kKeyF19 = 224, + kKeyF20 = 225, + kKeyF21 = 226, + kKeyF22 = 227, + kKeyF23 = 228, + kKeyF24 = 229, + kKeyF25 = 230, + kKeyF26 = 231, + kKeyF27 = 232, + kKeyF28 = 233, + kKeyF29 = 234, + kKeyF30 = 235, + kKeyF31 = 236, + kKeyF32 = 237, + kKeyF33 = 238, + kKeyF34 = 239, + kKeyF35 = 240, + kKeyF36 = 241, + kKeyF37 = 242, + kKeyF38 = 243, + kKeyF39 = 244, + kKeyF40 = 245, + kKeyF41 = 246, + kKeyF42 = 247, + kKeyF43 = 248, + kKeyF44 = 249, + kKeyF45 = 250, + kKeyF46 = 251, + kKeyF47 = 252, + kKeyF48 = 253, + kKeyF49 = 254, + kKeyF50 = 255, + kKeyF51 = 256, + kKeyF52 = 257, + kKeyF53 = 258, + kKeyF54 = 259, + kKeyF55 = 260, + kKeyF56 = 261, + kKeyF57 = 262, + kKeyF58 = 263, + kKeyF59 = 264, + kKeyF60 = 265, + kKeyF61 = 266, + kKeyF62 = 267, + kKeyF63 = 268, + kClrBol = 269, + kClearMargins = 270, + kSetLeftMargin = 271, + kSetRightMargin = 272, + kLabelFormat = 273, + kSetClock = 274, + kDisplayClock = 275, + kRemoveClock = 276, + kCreateWindow = 277, + kGotoWindow = 278, + kHangup = 279, + kDialPhone = 280, + kQuickDial = 281, + kTone = 282, + kPulse = 283, + kFlashHook = 284, + kFixedPause = 285, + kWaitTone = 286, + kUser0 = 287, + kUser1 = 288, + kUser2 = 289, + kUser3 = 290, + kUser4 = 291, + kUser5 = 292, + kUser6 = 293, + kUser7 = 294, + kUser8 = 295, + kUser9 = 296, + kOrigPair = 297, + kOrigColors = 298, + kInitializeColor = 299, + kInitializePair = 300, + kSetColorPair = 301, + kSetForeground = 302, + kSetBackground = 303, + kChangeCharPitch = 304, + kChangeLinePitch = 305, + kChangeResHorz = 306, + kChangeResVert = 307, + kDefineChar = 308, + kEnterDoublewideMode = 309, + kEnterDraftQuality = 310, + kEnterItalicsMode = 311, + kEnterLeftwardMode = 312, + kEnterMicroMode = 313, + kEnterNearLetterQuality = 314, + kEnterNormalQuality = 315, + kEnterShadowMode = 316, + kEnterSubscriptMode = 317, + kEnterSuperscriptMode = 318, + kEnterUpwardMode = 319, + kExitDoublewideMode = 320, + kExitItalicsMode = 321, + kExitLeftwardMode = 322, + kExitMicroMode = 323, + kExitShadowMode = 324, + kExitSubscriptMode = 325, + kExitSuperscriptMode = 326, + kExitUpwardMode = 327, + kMicroColumnAddress = 328, + kMicroDown = 329, + kMicroLeft = 330, + kMicroRight = 331, + kMicroRowAddress = 332, + kMicroUp = 333, + kOrderOfPins = 334, + kParmDownMicro = 335, + kParmLeftMicro = 336, + kParmRightMicro = 337, + kParmUpMicro = 338, + kSelectCharSet = 339, + kSetBottomMargin = 340, + kSetBottomMarginParm = 341, + kSetLeftMarginParm = 342, + kSetRightMarginParm = 343, + kSetTopMargin = 344, + kSetTopMarginParm = 345, + kStartBitImage = 346, + kStartCharSetDef = 347, + kStopBitImage = 348, + kStopCharSetDef = 349, + kSubscriptCharacters = 350, + kSuperscriptCharacters = 351, + kTheseCauseCr = 352, + kZeroMotion = 353, + kCharSetNames = 354, + kKeyMouse = 355, + kMouseInfo = 356, + kReqMousePos = 357, + kGetMouse = 358, + kSetAForeground = 359, + kSetABackground = 360, + kPkeyPlab = 361, + kDeviceType = 362, + kCodeSetInit = 363, + kSet0DesSeq = 364, + kSet1DesSeq = 365, + kSet2DesSeq = 366, + kSet3DesSeq = 367, + kSetLrMargin = 368, + kSetTbMargin = 369, + kBitImageRepeat = 370, + kBitImageNewline = 371, + kBitImageCarriageReturn = 372, + kColorNames = 373, + kDefineBitImageRegion = 374, + kEndBitImageRegion = 375, + kSetColorBand = 376, + kSetPageLength = 377, + kDisplayPcChar = 378, + kEnterPcCharsetMode = 379, + kExitPcCharsetMode = 380, + kEnterScancodeMode = 381, + kExitScancodeMode = 382, + kPcTermOptions = 383, + kScancodeEscape = 384, + kAltScancodeEsc = 385, + kEnterHorizontalHlMode = 386, + kEnterLeftHlMode = 387, + kEnterLowHlMode = 388, + kEnterRightHlMode = 389, + kEnterTopHlMode = 390, + kEnterVerticalHlMode = 391, + kSetAAttributes = 392, + kSetPglenInch = 393, +}; + +// An entry from the terminfo database. +// +// This class is thread-safe. +class TerminfoEntry final { + public: + // Looks up a terminfo entry in the system terminfo database. Throws + // std::runtime_error if no valid system entry exists for the specified + // terminal. + // + // This function is thread-hostile (it inspects the environment). + static TerminfoEntry FromSystemDatabase(absl::string_view terminal_name); + + // Parses an entry from binary data. Throws std::runtime_error if the data are + // invalid. + explicit TerminfoEntry(std::istream&); + + TerminfoEntry(const TerminfoEntry&) noexcept = default; + TerminfoEntry& operator=(const TerminfoEntry&) noexcept = default; + TerminfoEntry(TerminfoEntry&&) noexcept = default; + TerminfoEntry& operator=(TerminfoEntry&&) noexcept = default; + + const std::string& name() const noexcept { return names_[0]; } + const std::vector<std::string>& names() const noexcept { return names_; } + + bool get(BooleanCapability) const noexcept; + absl::optional<int> get(NumericCapability) const noexcept; + const absl::optional<std::string>& get(StringCapability) const noexcept; + + private: + std::vector<std::string> names_; + std::vector<int8_t> booleans_; + std::vector<int32_t> numbers_; + std::vector<absl::optional<std::string>> strings_; +}; + +} // namespace goldfishterm + +#endif // EC_GOLDFISHTERM_TERMINFO_H_ diff --git a/goldfishterm/terminfo_system_test.cc b/goldfishterm/terminfo_system_test.cc new file mode 100644 index 0000000..fe9ea9a --- /dev/null +++ b/goldfishterm/terminfo_system_test.cc @@ -0,0 +1,93 @@ +// 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 <getopt.h> +#include <locale.h> + +#include <iostream> +#include <stdexcept> +#include <string> + +#include "goldfishterm/terminfo.h" +#include "third_party/abseil/absl/strings/string_view.h" + +constexpr absl::string_view kShortUsage = + "Usage: find /usr/share/terminfo -type f -exec basename '{}' ';' | sort | " + "uniq |\n terminfo_system_test\n"; + +constexpr absl::string_view kHelp = R"( +Tests goldfishterm's terminfo parser against real-world binary terminfo files. +Reads terminal names, one per line, from standard input and attempts to look up +and parse the terminfo file associated with each one. Prints the terminal names +that failed lookup or parsing. + +A Reverse Polish scientific calculator. Invoked directly, ec launches an +interactive interpreter that displays the stack and accepts input. Invoked on a +file, ec processes the entire file; if the result stack is nonempty, ec prints +the stack in space-separated form. + +Options: + --help display this help and exit +)"; + +constexpr absl::string_view kAskForHelp = + "Try 'terminfo_system_test --help' for more information.\n"; + +enum { + kHelpLongOption = 128, +}; + +int main(int argc, char* argv[]) { + setlocale(LC_ALL, ""); + + static option long_options[] = { + {"help", no_argument, nullptr, kHelpLongOption}, + {nullptr, 0, nullptr, 0}, + }; + while (true) { + int c = getopt_long(argc, argv, "", long_options, /*longindex=*/nullptr); + if (c == -1) { + break; + } + switch (c) { + case kHelpLongOption: + std::cout << kShortUsage << kHelp; + return 0; + case '?': + std::cerr << kAskForHelp; + return 1; + default: + std::cerr << "terminfo_system_test: internal error: unhandled getopt " + << "switch\nThis is a bug! Please report it.\n"; + return 1; + } + } + + if (optind != argc) { + std::cerr << kShortUsage << kAskForHelp; + return 1; + } + + std::string line; + int r = 0; + while (std::getline(std::cin, line)) { + try { + goldfishterm::TerminfoEntry::FromSystemDatabase(line); + } catch (const std::runtime_error&) { + std::cout << line << '\n'; + r = 1; + } + } + return r; +} diff --git a/goldfishterm/terminfo_test.cc b/goldfishterm/terminfo_test.cc new file mode 100644 index 0000000..c50cb90 --- /dev/null +++ b/goldfishterm/terminfo_test.cc @@ -0,0 +1,228 @@ +// 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 "goldfishterm/terminfo.h" + +#include <errno.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <unistd.h> + +#include <filesystem> +#include <fstream> +#include <sstream> +#include <stdexcept> +#include <string> +#include <system_error> + +#include "third_party/abseil/absl/cleanup/cleanup.h" +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/types/optional.h" + +namespace goldfishterm { +namespace { + +using namespace std::literals::string_literals; + +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::IsEmpty; +using ::testing::Optional; + +void SetEnv(const char* var, const char* val) { + if (setenv(var, val, /*overwrite=*/1)) { + throw std::system_error(errno, std::generic_category(), "setenv"); + } +} + +// Creates a temporary directory for this test. +std::string MakeTemporaryDirectory() noexcept { + std::string dir = + absl::StrCat(testing::TempDir(), "/terminfo_test.", getpid()); + std::filesystem::create_directories(dir); + return dir; +} + +TEST(EntryParseTest, EmptyFails) { + std::istringstream s(""); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, BadMagicFails) { + std::istringstream s("foobar"); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, TooShortHeaderFails) { + std::istringstream s("\x1a\x01"); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, NegativeNamesLengthFails) { + std::istringstream s("\x1a\x01\xff\xff"); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, NegativeBooleansLengthFails) { + std::istringstream s("\x1a\x01\0\0\xff\xff"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, NegativeNumbersCountFails) { + std::istringstream s("\x1a\x01\0\0\0\0\xff\xff"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, NegativeStringsOffsetsCountFails) { + std::istringstream s("\x1a\x01\0\0\0\0\0\0\xff\xff"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, NegativeStringsLengthFails) { + std::istringstream s("\x1a\x01\0\0\0\0\0\0\0\0\xff\xff"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, MinimalTerminfo) { + std::istringstream s("\x1a\x01\0\0\0\0\0\0\0\0\0\0"s); + TerminfoEntry t(s); +} + +TEST(EntryParseTest, NamesNotNullTerminated) { + std::istringstream s("\x1a\x01\x04\0\0\0\0\0\0\0\0\0term"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, Names) { + std::istringstream s("\x1a\x01\x0f\0\0\0\0\0\0\0\0\0name|othername\0"s); + EXPECT_THAT(TerminfoEntry(s).names(), ElementsAre("name", "othername")); +} + +TEST(EntryParseTest, FirstName) { + std::istringstream s("\x1a\x01\x0f\0\0\0\0\0\0\0\0\0name|othername\0"s); + EXPECT_EQ(TerminfoEntry(s).name(), "name"); +} + +TEST(EntryParseTest, Booleans) { + std::istringstream s("\x1a\x01\x01\0\x04\0\0\0\0\0\0\0\0\x01\x00\x01\x00"s); + TerminfoEntry t(s); + EXPECT_TRUE(t.get(BooleanCapability::kAutoLeftMargin)); + EXPECT_FALSE(t.get(BooleanCapability::kAutoRightMargin)); + EXPECT_TRUE(t.get(BooleanCapability::kNoEscCtlc)); + EXPECT_FALSE(t.get(BooleanCapability::kCeolStandoutGlitch)); +} + +TEST(EntryParseTest, BooleanNegativeOneIsFalse) { + std::istringstream s("\x1a\x01\x01\0\x01\0\0\0\0\0\0\0\0\xff"s); + EXPECT_FALSE(TerminfoEntry(s).get(BooleanCapability::kAutoLeftMargin)); +} + +TEST(EntryParseTest, BooleanNegativeTwoIsFalse) { + std::istringstream s("\x1a\x01\x01\0\x01\0\0\0\0\0\0\0\0\xfe"s); + EXPECT_FALSE(TerminfoEntry(s).get(BooleanCapability::kAutoLeftMargin)); +} + +TEST(EntryParseTest, Numbers) { + std::istringstream s("\x1a\x01\x01\0\0\0\x02\0\0\0\0\0\0\0\x01\x02\x03\x04"s); + TerminfoEntry t(s); + EXPECT_EQ(t.get(NumericCapability::kColumns), 0x0201); + EXPECT_EQ(t.get(NumericCapability::kInitTabs), 0x0403); +} + +TEST(EntryParseTest, PaddingBeforeNumbersForAlignment) { + std::istringstream s("\x1a\x01\x01\0\0\0\x02\0\0\0\0\0\0\x01\x02\x03\x04"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, NumberNegativeOneOkay) { + std::istringstream s("\x1a\x01\x01\0\0\0\x01\0\0\0\0\0\0\0\xff\xff"s); + TerminfoEntry t(s); +} + +TEST(EntryParseTest, NumberNegativeTwoOkay) { + std::istringstream s("\x1a\x01\x01\0\0\0\x01\0\0\0\0\0\0\0\xfe\xff"s); + TerminfoEntry t(s); +} + +TEST(EntryParseTest, NumberNegativeThreeFails) { + std::istringstream s("\x1a\x01\x01\0\0\0\x01\0\0\0\0\0\0\0\xfd\xff"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, ExtendedNumberFormat) { + std::istringstream s("\x1e\x02\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\x02\x03\x04"s); + EXPECT_EQ(TerminfoEntry(s).get(NumericCapability::kColumns), 0x04030201); +} + +TEST(EntryParseTest, StringsWithoutOffsets) { + std::istringstream s("\x1a\x01\x01\0\0\0\0\0\0\0\x08\0\0\0foo\0bar\0"s); + TerminfoEntry t(s); + EXPECT_EQ(t.get(StringCapability::kBackTab), absl::nullopt); + EXPECT_EQ(t.get(StringCapability::kBell), absl::nullopt); +} + +TEST(EntryParseTest, ZeroOffsets) { + std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x02\0\0\0\0\0\0\0\0\0"s); + TerminfoEntry t(s); + EXPECT_THAT(t.get(StringCapability::kBackTab), Optional(IsEmpty())); + EXPECT_THAT(t.get(StringCapability::kBell), Optional(IsEmpty())); +} + +TEST(EntryParseTest, NegativeOneOffsetOkay) { + std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x01\0\0\0\0\0\xff\xff"s); + EXPECT_EQ(TerminfoEntry(s).get(StringCapability::kBackTab), absl::nullopt); +} + +TEST(EntryParseTest, NegativeTwoOffsetOkay) { + std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x01\0\0\0\0\0\xfe\xff"s); + EXPECT_EQ(TerminfoEntry(s).get(StringCapability::kBackTab), absl::nullopt); +} + +TEST(EntryParseTest, NegativeThreeOffsetFails) { + std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x01\0\0\0\0\0\xfd\xff"s); + EXPECT_THROW(TerminfoEntry t(s), std::runtime_error); +} + +TEST(EntryParseTest, Strings) { + std::istringstream s( + "\x1a\x01\x01\0\0\0\0\0\x02\0\x08\0\0\0\x04\0\0\0foo\0bar\0"s); + TerminfoEntry t(s); + EXPECT_THAT(t.get(StringCapability::kBackTab), Optional(Eq("bar"))); + EXPECT_THAT(t.get(StringCapability::kBell), Optional(Eq("foo"))); +} + +TEST(SystemTerminfoTest, NoSuchTerminal) { + std::string tmp = MakeTemporaryDirectory(); + absl::Cleanup remove_tmp = [&] { std::filesystem::remove_all(tmp); }; + SetEnv("TERMINFO", tmp.c_str()); + EXPECT_THROW(TerminfoEntry::FromSystemDatabase("vt100"), std::runtime_error); +} + +TEST(SystemTerminfoTest, LooksUpAdm3a) { + std::string tmp = MakeTemporaryDirectory(); + absl::Cleanup remove_tmp = [&] { std::filesystem::remove_all(tmp); }; + SetEnv("TERMINFO", tmp.c_str()); + + std::filesystem::create_directories(absl::StrCat(tmp, "/n")); + std::string binary_terminfo = + "\x1a\x01\x11\0\0\0\0\0\0\0\0\0notarealterminal\0"s; + std::ofstream(std::string(absl::StrCat(tmp, "/n/notarealterminal"))) + .write(binary_terminfo.data(), binary_terminfo.size()); + + EXPECT_EQ(TerminfoEntry::FromSystemDatabase("notarealterminal").name(), + "notarealterminal"); +} + +} // namespace +} // namespace goldfishterm diff --git a/scripts/run_all_tests b/scripts/run_all_tests index a73ca23..3282475 100755 --- a/scripts/run_all_tests +++ b/scripts/run_all_tests @@ -16,10 +16,22 @@ set -eu -o pipefail -readonly TESTS=(src/builtin_test src/language_test src/parser_driver_test) +readonly TESTS=( + goldfishterm/internal/string_capability_test + goldfishterm/terminfo_test + src/builtin_test + src/language_test + src/parser_driver_test +) +readonly MANUAL_TESTS=(goldfishterm/terminfo_system_test) cd "$(dirname "$(realpath "$0")")/.." -ninja "${TESTS[@]}" +ninja "${TESTS[@]}" "${MANUAL_TESTS[@]}" for test in "${TESTS[@]}"; do "./$test" done + +echo >&2 'Running terminfo system test; failed terminals are:' +if find /usr/share/terminfo -type f -exec basename '{}' ';' | goldfishterm/terminfo_system_test; then + echo >&2 'No failed terminals!' +fi diff --git a/src/ui/terminal/line.cc b/src/ui/terminal/line.cc index 8f156cb..0ae2d30 100644 --- a/src/ui/terminal/line.cc +++ b/src/ui/terminal/line.cc @@ -22,12 +22,15 @@ #include <algorithm> #include <iostream> +#include <memory> #include <stdexcept> #include <string> #include <system_error> #include <thread> #include <type_traits> +#include <utility> +#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" @@ -65,12 +68,6 @@ int TerminalColumns() { 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() { @@ -79,6 +76,7 @@ TerminalLine::TerminalLine() { } EnterRawMode(); + tty_ = std::make_unique<goldfishterm::SimpleTerminalOutput>(current_termios_); sigwinch_watcher_ = std::thread([this] { sigset_t sigwinch = SigsetContaining(SIGWINCH); @@ -109,16 +107,17 @@ void TerminalLine::SetLine(std::string 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. - WriteRaw(absl::StrCat(kBeginningOfLine, line_, ClearToEol())); + tty_->Write(line_); } 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())); + tty_->Write("..."); + tty_->Write(absl::string_view(&*line_.end() - to_display, to_display)); } + tty_->ClearToEndOfLine(); + tty_->Flush(); } char TerminalLine::GetChar() { @@ -139,21 +138,26 @@ char TerminalLine::GetChar() { void TerminalLine::Beep() { absl::MutexLock lock(&mu_); - WriteRaw("\a"); + tty_->Beep(); + tty_->Flush(); } void TerminalLine::PrintLine(absl::string_view message) { { absl::MutexLock lock(&mu_); - WriteRaw(absl::StrCat("\r\n", message, "\r\n")); + tty_->BeginningOfLine(); + tty_->CursorDown(); + tty_->Write(message); + tty_->BeginningOfLine(); + tty_->CursorDown(); } - Refresh(); + Refresh(); // includes a flush } void TerminalLine::EnterRawMode() { CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &original_termios_)); - current_termios_ = 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; @@ -184,19 +188,6 @@ void TerminalLine::ReportSigwinch() { 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", diff --git a/src/ui/terminal/line.h b/src/ui/terminal/line.h index a90e2b7..dfe86cb 100644 --- a/src/ui/terminal/line.h +++ b/src/ui/terminal/line.h @@ -19,8 +19,10 @@ #include <termios.h> +#include <memory> #include <thread> +#include "goldfishterm/simple.h" #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" @@ -81,6 +83,7 @@ class TerminalLine final { void WriteRaw(absl::string_view bytes) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); termios original_termios_, current_termios_; + std::unique_ptr<goldfishterm::SimpleTerminalOutput> tty_; std::thread sigwinch_watcher_; |