cfyzium / bearlibterminal

Interface library for applications with text-based console-like output
Other
119 stars 18 forks source link

Python read function always returns 224 #15

Closed pabrams closed 2 years ago

pabrams commented 2 years ago

contents of 224.py:

from bearlibterminal import terminal as term
key = term.read()
print('key:')
print(key)

Output:

me@pop-os:~/bearlibterminal$ python 224.py
key:
224 

What the heck is 224? https://github.com/tommyettinger/BearLibTerminal/issues/4

pabrams commented 2 years ago

I don't know why tommyettinger has more recent changes, nor why one of these is not a fork of the other.

pabrams commented 2 years ago

I'm also not understanding why my link, which clearly links to a tommyettinger location, somehow ends up back here when I click on it. Wtf is going on?

cfyzium commented 2 years ago

What the heck is 224?

224 is the decimal value of TK_CLOSE constant returned from read() when there was an attempt to close the window -- or when terminal instance has not yet been initialized by open().

Once upon a time it seemed a good idea to indicate closing/quitting condition if input processing started before window is visible on screen. Because you need a visible window to receive input from OS and without that any client code starting input read loop would just stuck indefinitely, effectively hanging the application. Returning a fake TK_CLOSE (which ideally should be handled by any input read loop) will break that loop and allow the app to exit. Or so was the reasoning.

A simpler solution might be to just make window visible the moment fist input processing function is called. But before open() there is no window to show, and if read() is called then? Ugh.

Well, I think the code may be refactored to not require explicit open() but instead lazily initializing resources when they are actually needed. That can make things asymmetric though, there is no open(), but who will clean up resources then?

why my link, which clearly links to a tommyettinger location, somehow ends up back here when I click on it

Because text and address of the link do not match.

pabrams commented 2 years ago

Because text and address of the link do not match.

Oops, I guess I was wrong about my link syntax. [http://somewhere]() doesn't link to http://somewhere, but redirects to current page. That might differ per flavor of markdown.

pabrams commented 2 years ago

What the heck is 224?

224 is the decimal value of TK_CLOSE constant returned from read() when there was an attempt to close the window -- or when terminal instance has not yet been initialized by open().

Once upon a time it seemed a good idea to indicate closing/quitting condition if input processing started before window is visible on screen. Because you need a visible window to receive input from OS and without that any client code starting input read loop would just stuck indefinitely, effectively hanging the application. Returning a fake TK_CLOSE (which ideally should be handled by any input read loop) will break that loop and allow the app to exit. Or so was the reasoning.

A simpler solution might be to just make window visible the moment fist input processing function is called. But before open() there is no window to show, and if read() is called then? Ugh.

Well, I think the code may be refactored to not require explicit open() but instead lazily initializing resources when they are actually needed. That can make things asymmetric though, there is no open(), but who will clean up resources then?

Sounds reasonable, but I don't think that's what I'm hitting. Here's a new repro:

224_open.py:

from bearlibterminal import terminal as term
term.open()
key = term.read()
print('key:')
print(key)
term.close()

Output:

me@pop-os:~/bearlibterminal$ python 224_open.py
key:
224

Terminal never opens, either, though. Do I need to wait for it or something? Docs talk about read blocking behavior, but I'm not sure it blocks by default (and from the docs, my interpretation is that's what it's supposed to do...)

Maybe open doesn't block...?

This doesn't work, either, though:

import time
from bearlibterminal import terminal as term

def sleep(start):
    print(f'Time: {time.time() - start:.2f}')
    time.sleep(1)

term.open()
sleep(time.time())
key = term.read()
print('key:')
print(key)
term.close()

output:

me@pop-os:~/bearlibterminal$ python go_open.py
Time: 0.00
key:
224
pabrams commented 2 years ago

I should mention, this seems to work okay with read_str(), but I'm trying to understand how read() works.

pabrams commented 2 years ago

This one works, but not if I replace read_str({parms}) with read():

import time
from bearlibterminal import terminal as term

def sleep(duration):
    print(f'Sleeping for {duration}')
    time.sleep(duration)

term.open()
sleep(2)
inlineKey = ''
key = term.read_str(1, 1, inlineKey, 1)
sleep(5)
print('key:')
print(key)
term.close()

As soon as I replace key = term.read_str(1, 1, inlineKey, 1) with key = term.read(), the terminal never opens, at all. Though it's clear that the sleeps still execute.

The output when using read_str:

me@pop-os:~/bearlibterminal$ python go_open.py
Sleeping for 2
Sleeping for 5
key:
(1, 'd')

and when using read:

me@pop-os:~/bearlibterminal$ python go_open.py
Sleeping for 2
Sleeping for 5
key:
224
pabrams commented 2 years ago

Oh, it's term.refresh that matters. If I add a refresh after the open, things make a lot mnore sense. though read() returns an integer, instead of a tuple, like read_str

pabrams commented 2 years ago

How do I get the result of a read? I'm getting integer 7 as a return value, when the input is 'd'.....

pabrams commented 2 years ago

Hmm, when the input is 'a', I get a 4... I guess I can use that knowledge to figure it out....

pabrams commented 2 years ago

We can probably close this issue. It doesn't make complete sense to me, but at least I can get it to do what I want, now.

There does seem to be some inconsistent behavior: read_str behaves well, even if I forget to refresh, but read, not so much.

Thanks for maintaining this.

cfyzium commented 2 years ago

Oh, it's term.refresh that matters.

Yeah I've mixed a few things up, sorry. The read() returns TK_CLOSE when the window is not yet visible, which is before both open() and refresh() have been called, open() is kind of general prerequisite and refresh() is the one that actually brings the window on screen.

I've been thinking about a few things to refactor, that's where the part about implicit open() came from. In this particular case, it is refresh() that should be implicit when calling any other OS-interacting function. This still leaves the question how to deal with missing open() call, because not even implicit refresh() would be able to show a window before that. A few things do not really fit mandatory explicit open()/close(), and that's what I've been mulling over.

though read() returns an integer, instead of a tuple, like read_str

That's because read() waits and returns a single event (e. g. keypress), while read_str() is a utility function that interactively reads a string and returns it along with a status. The returned tuple is (rc, str) where rc is either the length of string read if user has confirmed input by pressing ENTER, or TK_CANCELLED if user has interrupted input by pressing ESCAPE.

How do I get the result of a read? I'm getting integer 7 as a return value, when the input is 'd'.....

read() returns one of event TK_xxx constants listed here: event_and_state_constants. When you press d, you'll get 0x07 which is TK_D.

If you want to read individual symbols, then you will have to query TK_WCHAR state that holds Unicode codepoint of the last event, or 0 if the last event did not produce text input (I assume Python3 and Unicode strings):

from bearlibterminal import terminal
terminal.open()
terminal.refresh()
s = ''
while True:
  key = terminal.read()
  if key == terminal.TK_CLOSE:
    break
  elif key == terminal.TK_BACKSPACE:
    s = s[:-1]
  else:
    c = terminal.state(terminal.TK_WCHAR)
    if c > 0:
      s += chr(c)
  terminal.clear()
  terminal.print(2, 1, s)
  terminal.refresh()
terminal.close()

As a side node, there is another issue that may require some refactoring. A single character being associated with a single keypress is a naïve oversimplification that works for a few simpler keyboard layouts. Internationalized input is very complex and OS can report it without clear association with text keys being pressed, e. g. a key may produce more than one character or there may not even be a detectable key press at all. It seem like low-level text input should be a separate event with string value from the OS (just like SDL2 does it). But a string value won't be accessible through integer state() and this little, cute and clunky interface of event codes and states starts to fall apart.

There does seem to be some inconsistent behavior: read_str behaves well, even if I forget to refresh, but read, not so much.

That's because read_str() is a utility function that does interactive input with visual feedback. It has to draw stuff and therefore uses refresh() internally.

time.sleep(duration)

You should use term.delay(duration * 1000) instead. Using library's own function allows it to keep processing OS events during the pause, keeping the window responsive. Otherwise the window is essentially hanging up for the duration of the pause and may behave funny, e. g. try minimizing it while paused.