Instead of true/false I use a value of 2/-2 for a recent change (2 for a new press, -2 for a new release), then at the beginning of the loop, before going through the new events I go through the whole array and change every 2 or -2 to 1 or -1. I use 3 in case of a key repeat, so I can handle all those different things in the rest of my code smoothly. So if I test the key for ==2 it means only new presses, if I test for >=2 it means new presses and repeats, >=1 is if it's down at all and so on.
But if you don't want to directly use numbers (probably best not to if other people need to understand your code) you can use the following enum:
KEYUP_NEW = -2,
and then you only have to test for
if (keys[key]==KEYDOWN_NEW) instead of the less readable
if (!old_keys[key] && keys[key]) whose meaning isn't as instantly obvious. You can even go one better by having a function do the test for you so that
test_key(key, KEYDOWN) or
is_key_down(key) can return positive whether the value is KEYDOWN, KEYDOWN_NEW or KEYDOWN_REPEAT if you don't want to use the less obvious
But there's a big problem with your approach, it's that you'll miss keypresses that last shorter than your average loop because in those cases you'll have a down event instantly followed by a up event, and therefore your table as seen from outside of the event polling loop will miss the keypress entirely. I have a solution for this, it's to have another table and when you detect a down event while your entry in your table is 2 then don't change it to -2 but rather set the matching entry in that other table to 1. This way for that one iteration of the loop your key is set to 2, which is what you want. Then when the next loop starts, at the same time as you turn 2s into 1s you check that other table and if it's set to 1 then you turn the 2 into a -2 so that you signal the release.