ndreynolds / ratatouille

A TUI (terminal UI) kit for Elixir
MIT License
752 stars 39 forks source link

Redesigning the terminal event messages #6

Open ndreynolds opened 5 years ago

ndreynolds commented 5 years ago

Current Situtation

Currently, the runtime / event manager sends terminal events as messages to the application in the following format:

{:event, 
  %ExTermbox.Event{
    ch: 0, 
    h: 38, 
    key: 0, 
    mod: 0, 
    type: 2, 
    w: 147, 
    x: 0, 
    y: 0
  }}

The ExTermbox.Event struct is directly based off of the C struct from the termbox API, which uses integer codes for the event type, key (based on terminfo), modifier, character, etc.:

https://github.com/nsf/termbox/blob/master/src/termbox.h

So far, the recommended way to match events on a certain key has been to match the integer for that key by first looking up the constant, e.g.:

@ctrl_c Constants.key(:ctrl_c)

case message do
  {:event, %{key: @ctrl_c}} ->
    # handle ctrl-c keypress

This project evolved out of the termbox bindings library, so this event API made sense in the beginning.

Problems

Constants are clumsy

Looking up constants and storing them in an attribute just to pattern match on them feels clumsy and is an unnecessary extra step. It should be possible to simplify the above as such:

case message do
  {:event, %{key: :ctrl_c}} ->
    # handle ctrl-c keypress

However, one issue with this is that some keys are defined to have the same integer value. For example, ctrl-~ (tilde) and ctrl-2 are both set to 0x00. Which begs the question: which one do we set as key above?

Not integrated with views and rendering

So far, events are completely separate from the views. The "target" of an event is always the terminal. I think we can do better than that. If I click on a label element, it should be possible for the rendering library to figure out that I've clicked that element and expose that information via the event.

Union type has a lot of extraneous information

The event struct is really a sort of union type---each event has a different type and depending on the type, certain information will be filled in and other information will be blank. From a usability standpoint, this can be very confusing.

Ideas

Extended events

In order to (mostly) maintain backwards compatibility, we could take the existing ExTermbox.Event{} struct and extend it with computed information, e.g.:

%Ratatouille.Event{
  ch: 0,
  key: 1,
  key_name: :ctrl_a,
  mod: 0,
  mod_name: :alt,
  type: 1,
  type_name: :key,
  w: 0,
  h: 0,
  x: 0,
  y: 0,
  target: nil
}

AFAIK, it would only break code that explicitly matches the struct (vs. matching any map with the keys).

New event structs by type (Breaking change)

Another approach would be to totally overhaul it in a new major version:

%Ratatouille.KeyEvent{
  key: :a,
  mod: :alt,
  key_code: 0,
  character_code: 97
}

%Ratatouille.MouseEvent{
  x: 21,
  y: 56,
  target: %Element{tag: :label, ...}
}

%Ratatouille.ResizeEvent{
  width: 45,
  height: 21
}

These would also be sent as messages in a new style:

{:key, %KeyEvent{}}
{:mouse, %MouseEvent{}}
{:resize, %ResizeEvent{}}

Other Ideas ???

Next Steps

I'd like to get some feedback and let the problem simmer for a while before I start changing anything.