aboutsummaryrefslogtreecommitdiff
path: root/ttwhy/terminal/echo.cppm
blob: 5aefdae7a4071a897f33c81b5f2725000b36db82 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
module;

#include <asio/as_tuple.hpp>
#include <asio/awaitable.hpp>
#include <asio/buffer.hpp>
#include <asio/use_awaitable.hpp>
#include <asio/write.hpp>

#include <algorithm>

#include <charconv>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <format>
#include <string>
#include <string_view>
#include <type_traits>
#include <variant>

export module ttwhy.terminal:echo;

import :events;
import ttwhy.core;

namespace ttwhy::terminal
{

  using namespace std::string_view_literals;

  constexpr auto vte_backspace_sequence = "\b \b"sv;
  constexpr auto vte_newline_sequence = "\r\n"sv;
  constexpr auto vte_horizontal_tab_sequence = "\t"sv;
  constexpr auto vte_cursor_left_sequence = "\x1b[D"sv;
  constexpr auto vte_cursor_right_sequence = "\x1b[C"sv;
  constexpr auto vte_delete_character_sequence = "\x1b[P"sv;
  constexpr auto vte_cursor_left_sequence_format = "\x1b[{}D"sv;
  constexpr auto vte_cursor_right_sequence_format = "\x1b[{}C"sv;

  namespace detail
  {
    //! Maps a function key to its CSI parameter code (F5 through F20).
    auto function_key_parameter(function_key key) -> unsigned int
    {
      switch (key)
      {
        case function_key::f5:
          return 15;
        case function_key::f6:
          return 17;
        case function_key::f7:
          return 18;
        case function_key::f8:
          return 19;
        case function_key::f9:
          return 20;
        case function_key::f10:
          return 21;
        case function_key::f11:
          return 23;
        case function_key::f12:
          return 24;
        case function_key::f13:
          return 25;
        case function_key::f14:
          return 26;
        case function_key::f15:
          return 28;
        case function_key::f16:
          return 29;
        case function_key::f17:
          return 31;
        case function_key::f18:
          return 32;
        case function_key::f19:
          return 33;
        case function_key::f20:
          return 34;
        default:
          return 0;
      }
    }

    //! Appends an xterm-style modifier parameter to a CSI sequence.
    auto apply_modifier(std::string sequence, modifier_set modifiers) -> std::string
    {
      auto const mask = static_cast<std::uint8_t>(modifiers);
      auto const modifier_param = [&mask]() -> unsigned int {
        if ((mask & static_cast<std::uint8_t>(modifier::shift)) != 0)
        {
          return 2;
        }
        if ((mask & static_cast<std::uint8_t>(modifier::alt)) != 0)
        {
          return 3;
        }
        if ((mask & static_cast<std::uint8_t>(modifier::ctrl)) != 0)
        {
          return 4;
        }
        if ((mask & static_cast<std::uint8_t>(modifier::meta)) != 0)
        {
          return 5;
        }
        return 1;
      }();

      if (modifier_param == 1)
      {
        return sequence;
      }

      auto const terminator = sequence.find_last_of('~');
      if (terminator == std::string::npos)
      {
        return sequence;
      }
      return sequence.substr(0, terminator) + std::to_string(modifier_param) + sequence.substr(terminator);
    }

    //! Builds the terminal escape sequence that represents a function key press.
    auto function_key_sequence(function_key key, modifier_set modifiers) -> std::string
    {
      switch (key)
      {
        case function_key::f1:
          return apply_modifier(std::string{"\x1bOP"}, modifiers);
        case function_key::f2:
          return apply_modifier(std::string{"\x1bOQ"}, modifiers);
        case function_key::f3:
          return apply_modifier(std::string{"\x1bOR"}, modifiers);
        case function_key::f4:
          return apply_modifier(std::string{"\x1bOS"}, modifiers);
        default:
          return apply_modifier(std::format("\x1b[{}~", function_key_parameter(key)), modifiers);
      }
    }
  }  // namespace detail

  //! An asynchronous router that echoes characters and handles terminal navigation.
  //!
  //! @tparam StreamType The output stream descriptor type.
  export template<typename StreamType>
  struct echo
  {
    //! Construct an echo router writing to an output stream.
    //!
    //! @param stream The output stream to write echoed text and escape sequences to.
    explicit echo(StreamType & stream)
        : m_output_stream{stream}
    {}

    //! Get the current cursor position within the printed line.
    //!
    //! @return The zero-based cursor position.
    [[nodiscard]] auto cursor_position() const noexcept -> std::size_t
    {
      return m_cursor_position;
    }

    //! Get the maximum cursor position reached on the current line.
    //!
    //! @return The maximum position reached.
    [[nodiscard]] auto max_position() const noexcept -> std::size_t
    {
      return m_max_position;
    }

    //! Asynchronously process an incoming terminal input event.
    //!
    //! @param event The input event to handle.
    //! @return An awaitable task that completes when processing finishes.
    auto process(input_event event) -> asio::awaitable<void>
    {
      co_await std::visit(
          [&](auto && event) -> asio::awaitable<void> {
            using event_type = std::decay_t<decltype(event)>;

            if constexpr (std::same_as<character_event, event_type>)
            {
              co_await process_character(event.value);
            }
            else if constexpr (std::same_as<control_event, event_type>)
            {
              co_await process_control_key(event.key);
            }
            else if constexpr (std::same_as<navigation_event, event_type>)
            {
              co_await process_navigation_key(event);
            }
            else if constexpr (std::same_as<function_key_event, event_type>)
            {
              co_await process_function_key(event);
            }
            else if constexpr (std::same_as<ctrl_chord_event, event_type>)
            {
              co_await process_ctrl_chord(event);
            }
          },
          event);
    }

  private:
    auto process_function_key(function_key_event event) -> asio::awaitable<void>
    {
      auto const sequence = detail::function_key_sequence(event.key, event.modifiers);
      co_await asio::async_write(m_output_stream, asio::buffer(sequence.data(), sequence.size()),
                                 asio::as_tuple(asio::use_awaitable));
    }

    auto process_ctrl_chord(ctrl_chord_event event) -> asio::awaitable<void>
    {
      auto const control_character = static_cast<char>(static_cast<unsigned char>(event.key) - 0x40u);
      co_await asio::async_write(m_output_stream, asio::buffer(&control_character, 1),
                                 asio::as_tuple(asio::use_awaitable));
    }

    auto process_character(char character) -> asio::awaitable<void>
    {
      auto [error, bytes_written] =
          co_await asio::async_write(m_output_stream, asio::buffer(&character, 1), asio::as_tuple(asio::use_awaitable));
      if (!error)
      {
        m_cursor_position += bytes_written;
        m_max_position = std::max(m_max_position, m_cursor_position);
      }
    }

    auto process_control_key(control_key key) -> asio::awaitable<void>
    {
      switch (key)
      {
        case control_key::backspace:
          if (m_cursor_position > 0)
          {
            auto [error, bytes_written] = co_await asio::async_write(
                m_output_stream, asio::buffer(vte_backspace_sequence), asio::as_tuple(asio::use_awaitable));
            if (!error)
            {
              if (m_cursor_position == m_max_position)
              {
                --m_max_position;
              }
              --m_cursor_position;
            }
          }
          break;
        case control_key::enter:
        {
          auto [error, bytes_written] = co_await asio::async_write(m_output_stream, asio::buffer(vte_newline_sequence),
                                                                   asio::as_tuple(asio::use_awaitable));
          if (!error)
          {
            m_cursor_position = 0;
            m_max_position = 0;
          }
          break;
        }
        case control_key::tab:
        {
          auto [error, bytes_written] = co_await asio::async_write(
              m_output_stream, asio::buffer(vte_horizontal_tab_sequence), asio::as_tuple(asio::use_awaitable));
          if (!error)
          {
            auto const tab_stop = 8uz - (m_cursor_position % 8uz);
            m_cursor_position += tab_stop;
            m_max_position = std::max(m_max_position, m_cursor_position);
          }
          break;
        }
        default:
          co_await asio::async_write(m_output_stream, asio::buffer(std::format("{{CTRL:{}}}", std::to_underlying(key))),
                                     asio::as_tuple(asio::use_awaitable));
          break;
      };
    }

    auto process_navigation_key(navigation_event event) -> asio::awaitable<void>
    {
      switch (event.key)
      {
        case navigation_key::left:
          if (m_cursor_position > 0)
          {
            auto [error, bytes_written] = co_await asio::async_write(
                m_output_stream, asio::buffer(vte_cursor_left_sequence), asio::as_tuple(asio::use_awaitable));
            if (!error)
            {
              --m_cursor_position;
            }
          }
          break;

        case navigation_key::right:
          if (m_cursor_position < m_max_position)
          {
            auto [error, bytes_written] = co_await asio::async_write(
                m_output_stream, asio::buffer(vte_cursor_right_sequence), asio::as_tuple(asio::use_awaitable));
            if (!error)
            {
              ++m_cursor_position;
            }
          }
          break;

        case navigation_key::home:
          if (m_cursor_position > 0)
          {
            auto sequence = std::format(vte_cursor_left_sequence_format, m_cursor_position);
            auto [error, bytes_written] = co_await asio::async_write(m_output_stream, asio::buffer(sequence),
                                                                     asio::as_tuple(asio::use_awaitable));
            if (!error)
            {
              m_cursor_position = 0;
            }
          }
          break;

        case navigation_key::end:
          if (m_cursor_position < m_max_position)
          {
            auto const distance = m_max_position - m_cursor_position;
            auto sequence = std::format(vte_cursor_right_sequence_format, distance);
            auto [error, bytes_written] = co_await asio::async_write(m_output_stream, asio::buffer(sequence),
                                                                     asio::as_tuple(asio::use_awaitable));
            if (!error)
            {
              m_cursor_position = m_max_position;
            }
          }
          break;

        case navigation_key::delete_key:
          if (m_cursor_position < m_max_position)
          {
            auto [error, bytes_written] = co_await asio::async_write(
                m_output_stream, asio::buffer(vte_delete_character_sequence), asio::as_tuple(asio::use_awaitable));
            if (!error)
            {
              --m_max_position;
            }
          }
          break;

        default:
          break;
      }
    }

    StreamType & m_output_stream;
    std::size_t m_cursor_position{};
    std::size_t m_max_position{};
  };

}  // namespace ttwhy::terminal